Compare commits

..

No commits in common. "master" and "v1.0.0" have entirely different histories.

36 changed files with 13845 additions and 1759 deletions

View File

@ -1,21 +0,0 @@
name: libcron tests
on: [push, pull_request]
jobs:
Tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Build
run: |
mkdir build
cd build
cmake ..
make -j4
- name: Test
run: |
cd test/out
./cron_test

1
.gitignore vendored
View File

@ -34,4 +34,3 @@
cmake-build-* cmake-build-*
.idea/workspace.xml .idea/workspace.xml
out/* out/*
test/out*

3
.gitmodules vendored
View File

@ -1,6 +1,3 @@
[submodule "libcron/externals/date"] [submodule "libcron/externals/date"]
path = libcron/externals/date path = libcron/externals/date
url = https://github.com/HowardHinnant/date.git url = https://github.com/HowardHinnant/date.git
[submodule "test/externals/Catch2"]
path = test/externals/Catch2
url = https://github.com/catchorg/Catch2.git

29
.idea/codeStyles/Project.xml generated Normal file
View File

@ -0,0 +1,29 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<Objective-C-extensions>
<file>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
</file>
<class>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
</class>
<extensions>
<pair source="cpp" header="h" fileNamingConvention="NONE" />
<pair source="c" header="h" fileNamingConvention="NONE" />
</extensions>
</Objective-C-extensions>
</code_scheme>
</component>

1
.idea/vcs.xml generated
View File

@ -2,6 +2,5 @@
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" /> <mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/libcron/externals/date" vcs="Git" />
</component> </component>
</project> </project>

View File

@ -1,12 +1,9 @@
cmake_minimum_required(VERSION 3.6) cmake_minimum_required(VERSION 3.6)
project(top) set(OUTPUT_LOCATION ${CMAKE_CURRENT_LIST_DIR}/out/)
add_subdirectory(libcron) add_subdirectory(libcron)
add_subdirectory(test) add_subdirectory(test)
add_dependencies(cron_test libcron) add_dependencies(cron_test libcron)
install(TARGETS libcron DESTINATION lib)
install(DIRECTORY libcron/include/libcron DESTINATION include)
install(DIRECTORY libcron/externals/date/include/date DESTINATION include)

173
README.md
View File

@ -1,131 +1,7 @@
# libcron # libcron
A C++ scheduling library using cron formatting. A C++ scheduling library using cron formatting.
# Using the Scheduler # Local time vs UTC
Libcron offers an easy to use API to add callbacks with corresponding cron-formatted strings:
```
libcron::Cron cron;
cron.add_schedule("Hello from Cron", "* * * * * ?", [=](auto&) {
std::cout << "Hello from libcron!" std::endl;
});
```
To trigger the execution of callbacks, one must call `libcron::Cron::tick` at least once a second to prevent missing schedules:
```
while(true)
{
cron.tick();
std::this_thread::sleep_for(500mS);
}
```
In case there is a lot of time between you call `add_schedule` and `tick`, you can call `recalculate_schedule`.
The callback must have the following signature:
```
std::function<void(const libcron::TaskInformation&)>
```
`libcron::Taskinformation` offers a convenient API to retrieve further information:
- `libcron::TaskInformation::get_delay` informs about the delay between planned and actual execution of the callback. Hence, it is possible to ensure that a task was executed within a specific tolerance:
```
libcron::Cron cron;
cron.add_schedule("Hello from Cron", "* * * * * ?", [=](auto& i) {
using namespace std::chrono_literals;
if (i.get_delay() >= 1s)
{
std::cout << "The Task was executed too late..." << std::endl;
}
});
```
- `libcron::TaskInformation::get_name` gives you the name of the current Task. This allows to add attach the same callback to multiple schedules:
```
libcron::Cron cron;
auto f = [](auto& i) {
if (i.get_name() == "Task 1")
{
do_work_task_1();
}
else if (i.get_name() == "Task 2")
{
do_work_task_2();
}
};
cron.add_schedule("Task 1", "* * * * * ?", f);
cron.add_schedule("Task 2", "* * * * * ?", f);
```
## Adding multiple tasks with individual schedules at once
libcron::cron::add_schedule needs to sort the underlying container each time you add a schedule. To improve performance when adding many tasks by only sorting once, there is a convinient way to pass either a `std::map<std::string, std::string>`, a `std::vector<std::pair<std::string, std::string>>`, a `std::vector<std::tuple<std::string, std::string>>` or a `std::unordered_map<std::string, std::string>` to `add_schedule`, where the first element corresponds to the task name and the second element to the task schedule. Only if all schedules in the container are valid, they will be added to `libcron::Cron`. The return type is a `std::tuple<bool, std::string, std::string>`, where the boolean is `true` if the schedules have been added or false otherwise. If the schedules have not been added, the second element in the tuple corresponds to the task-name with the given invalid schedule. If there are multiple invalid schedules in the container, `add_schedule` will abort at the first invalid element:
```
std::map<std::string, std::string> name_schedule_map;
for(int i = 1; i <= 1000; i++)
{
name_schedule_map["Task-" + std::to_string(i)] = "* * * * * ?";
}
name_schedule_map["Task-1000"] = "invalid";
auto res = c1.add_schedule(name_schedule_map, [](auto&) { });
if (std::get<0>(res) == false)
{
std::cout << "Task " << std::get<1>(res)
<< "has an invalid schedule: "
<< std::get<2>(res) << std::endl;
}
```
## Removing schedules from `libcron::Cron`
libcron::Cron offers two convenient functions to remove schedules:
- `clear_schedules()` will remove all schedules
- `remove_schedule(std::string)` will remove a specific schedule
For example, `cron.remove_schedule("Hello from Cron")` will remove the previously added task.
## Removing/Adding tasks at runtime in a multithreaded environment
When Calling `libcron::Cron::tick` from another thread than `add_schedule`, `clear_schedule` and `remove_schedule`, one must take care to protect the internal resources of `libcron::Cron` so that tasks are not removed or added while `libcron::Cron` is iterating over the schedules. `libcron::Cron` can take care of that, you simply have to define your own aliases:
```
/* The default class uses NullLock, which does not lock the resources at runtime */
template<typename ClockType = libcron::LocalClock, typename LockType = libcron::NullLock>
class Cron
{
...
}
/* Define an alias for a thread-safe Cron scheduler which automatically locks ressources when needed */
using CronMt = libcron::Cron<libcron::LocalClock, libcron::Locker>
CronMt cron;
cron.add_schedule("Hello from Cron", "* * * * * ?", [=]() {
std::cout << "Hello from CronMt!" std::endl;
});
....
```
However, this comes with costs: Whenever you call `tick`, a `std::mutex` will be locked and unlocked. So only use the `libcron::Locker` to protect resources when you really need too.
## Local time vs UTC
This library uses `std::chrono::system_clock::timepoint` as its time unit. While that is UTC by default, the Cron-class This library uses `std::chrono::system_clock::timepoint` as its time unit. While that is UTC by default, the Cron-class
uses a `LocalClock` by default which offsets `system_clock::now()` by the current UTC-offset. If you wish to work in uses a `LocalClock` by default which offsets `system_clock::now()` by the current UTC-offset. If you wish to work in
@ -179,54 +55,19 @@ Each part is separated by one or more whitespaces. It is thus important to keep
* Invalid: * Invalid:
* 0, 3, 40-50 * * * * ? * 0, 3, 40-50 * * * * ?
`Day of month` and `day of week` are mutually exclusive so one of them must at always be ignored using `Day of month` and `day of week` are mutually exclusive so one of them must at always be ignored using
the '?'-character to ensure that it is not possible to specify a statement which results in an impossible mix of these fields. the '?'-character unless one field already is something other than '*'.
## Examples # Examples
|Expression | Meaning |Expression | Meaning
| --- | --- | | --- | --- |
| * * * * * ? | Every second | * * * * * ? | Every second
| 0 * * * * ? | Every minute |0 0 12 * * MON-FRI | Every Weekday at noon
| 0 0 12 * * MON-FRI | Every Weekday at noon |0 0 12 1/2 * ? | Every 2 days, starting on the 1st at noon
| 0 0 12 1/2 * ? | Every 2 days, starting on the 1st at noon
| 0 0 */12 ? * * | Every twelve hours | 0 0 */12 ? * * | Every twelve hours
| @hourly | Every hour
Note that the expression formatting has a part for seconds and the day of week. # Third party libraries
For the day of week part, a question mark ? is utilized. This format
may not be parsed by all online crontab calculators or expression generators.
## Convenience scheduling
These special time specification tokens which replace the 5 initial time and date fields, and are prefixed with the '@' character, are supported:
|Token|Meaning
| --- | --- |
| @yearly | Run once a year, ie. "0 0 1 1 *".
| @annually | Run once a year, ie. "0 0 1 1 *".
| @monthly | Run once a month, ie. "0 0 1 * *".
| @weekly | Run once a week, ie. "0 0 * * 0".
| @daily | Run once a day, ie. "0 0 * * *".
| @hourly | Run once an hour, ie. "0 * * * *".
# Randomization
The standard cron format does not allow for randomization, but with the use of `CronRandomization` you can generate random
schedules using the following format: `R(range_start-range_end)`, where `range_start` and `range_end` follow the same rules
as for a regular cron range (step-syntax is not supported). All the rules for a regular cron expression still applies
when using randomization, i.e. mutual exclusiveness and no extra spaces.
## Examples
|Expression | Meaning
| --- | --- |
| 0 0 R(13-20) * * ? | On the hour, on a random hour 13-20, inclusive.
| 0 0 0 ? * R(0-6) | A random weekday, every week, at midnight.
| 0 R(45-15) */12 ? * * | A random minute between 45-15, inclusive, every 12 hours.
|0 0 0 ? R(DEC-MAR) R(SAT-SUN)| On the hour, on a random month december to march, on a random weekday saturday to sunday.
# Used Third party libraries
Howard Hinnant's [date libraries](https://github.com/HowardHinnant/date/) Howard Hinnant's [date libraries](https://github.com/HowardHinnant/date/)

View File

@ -1,46 +1,30 @@
cmake_minimum_required(VERSION 3.6) cmake_minimum_required(VERSION 3.6)
project(libcron) project(libcron)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 14)
# Deactivate Iterator-Debugging on Windows
option(LIBCRON_DEACTIVATE_ITERATOR_DEBUGGING "Build with iterator-debugging (MSVC only)." OFF)
if( MSVC ) if( MSVC )
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4")
if (LIBCRON_DEACTIVATE_ITERATOR_DEBUGGING)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_HAS_ITERATOR_DEBUGGING=0")
endif()
else() else()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic")
endif() endif()
include_directories(${CMAKE_CURRENT_LIST_DIR}/externals/date/include)
add_library(${PROJECT_NAME} add_library(${PROJECT_NAME}
include/libcron/Cron.h Cron.h
include/libcron/CronClock.h Task.h
include/libcron/CronData.h CronData.h
include/libcron/CronRandomization.h TimeTypes.h
include/libcron/CronSchedule.h CronData.cpp
include/libcron/DateTime.h CronSchedule.cpp
include/libcron/Task.h CronSchedule.h
include/libcron/TimeTypes.h DateTime.h
src/CronClock.cpp Task.cpp
src/CronData.cpp CronClock.h
src/CronRandomization.cpp CronClock.cpp)
src/CronSchedule.cpp
src/Task.cpp)
target_include_directories(${PROJECT_NAME}
PRIVATE ${CMAKE_CURRENT_LIST_DIR}/externals/date/include
PUBLIC include)
if(NOT MSVC)
# Assume a modern compiler (gcc 9.3)
target_compile_definitions (${PROJECT_NAME} PRIVATE -DHAS_UNCAUGHT_EXCEPTIONS)
endif()
set_target_properties(${PROJECT_NAME} PROPERTIES set_target_properties(${PROJECT_NAME} PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out/${CMAKE_BUILD_TYPE}" ARCHIVE_OUTPUT_DIRECTORY "${OUTPUT_LOCATION}"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out/${CMAKE_BUILD_TYPE}" LIBRARY_OUTPUT_DIRECTORY "${OUTPUT_LOCATION}"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out/${CMAKE_BUILD_TYPE}") RUNTIME_OUTPUT_DIRECTORY "${OUTPUT_LOCATION}")

220
libcron/Cron.h Normal file
View File

@ -0,0 +1,220 @@
#pragma once
#include <string>
#include <chrono>
#include <queue>
#include <memory>
#include "Task.h"
#include "CronClock.h"
namespace libcron
{
template<typename ClockType>
class Cron;
template<typename ClockType>
std::ostream& operator<<(std::ostream& stream, const Cron<ClockType>& c);
template<typename ClockType = libcron::LocalClock>
class Cron
{
public:
bool add_schedule(std::string name, const std::string& schedule, std::function<void()> work);
size_t count() const
{
return tasks.size();
}
// Tick is expected to be called at least once a second to prevent missing schedules.
size_t
tick()
{
return tick(clock.now());
}
size_t
tick(std::chrono::system_clock::time_point now);
std::chrono::system_clock::duration
time_until_next() const;
ClockType& get_clock()
{
return clock;
}
void get_time_until_expiry_for_tasks(
std::vector<std::tuple<std::string, std::chrono::system_clock::duration>>& status) const;
friend std::ostream& operator<<<>(std::ostream& stream, const Cron<ClockType>& c);
private:
class Queue
// Priority queue placing smallest (i.e. nearest in time) items on top.
: public std::priority_queue<Task, std::vector<Task>, std::greater<>>
{
public:
// Inherit to allow access to the container.
const std::vector<Task>& get_tasks() const
{
return c;
}
std::vector<Task>& get_tasks()
{
return c;
}
};
Queue tasks{};
ClockType clock{};
bool first_tick = true;
std::chrono::system_clock::time_point last_tick{};
};
template<typename ClockType>
bool Cron<ClockType>::add_schedule(std::string name, const std::string& schedule, std::function<void()> work)
{
auto cron = CronData::create(schedule);
bool res = cron.is_valid();
if (res)
{
Task t{std::move(name), CronSchedule{cron}, std::move(work)};
if (t.calculate_next(clock.now()))
{
tasks.push(t);
}
}
return res;
}
template<typename ClockType>
std::chrono::system_clock::duration Cron<ClockType>::time_until_next() const
{
std::chrono::system_clock::duration d{};
if (tasks.empty())
{
d = std::numeric_limits<std::chrono::minutes>::max();
}
else
{
d = tasks.top().time_until_expiry(clock.now());
}
return d;
}
template<typename ClockType>
size_t Cron<ClockType>::tick(std::chrono::system_clock::time_point now)
{
size_t res = 0;
if(!first_tick)
{
// Only allow time to flow if at least one second has passed since the last tick,
// either forward or backward.
auto diff = now - last_tick;
constexpr auto one_second = std::chrono::seconds{1};
if(diff < one_second && diff > -one_second)
{
now = last_tick;
}
}
if (first_tick)
{
first_tick = false;
}
else
{
// https://linux.die.net/man/8/cron
constexpr auto three_hours = std::chrono::hours{3};
auto diff = now - last_tick;
auto absolute_diff = diff > diff.zero() ? diff : -diff;
if(absolute_diff >= three_hours)
{
// Time changes of more than 3 hours are considered to be corrections to the
// clock or timezone, and the new time is used immediately.
for (auto& t : tasks.get_tasks())
{
t.calculate_next(now);
}
}
else
{
// Change of less than three hours
// If time has moved backwards: Since tasks are not rescheduled, they won't run before
// we're back at least the original point in time which prevents running tasks twice.
// If time has moved forward, tasks that would have run since last tick will be run.
}
}
last_tick = now;
std::vector<Task> executed{};
while (!tasks.empty()
&& tasks.top().is_expired(now))
{
executed.push_back(tasks.top());
tasks.pop();
auto& t = executed[executed.size() - 1];
t.execute(now);
}
res = executed.size();
// Place executed tasks back onto the priority queue.
std::for_each(executed.begin(), executed.end(), [this, &now](Task& task)
{
// Must calculate new schedules using second after 'now', otherwise
// we'll run the same task over and over if it takes less than 1s to execute.
using namespace std::chrono_literals;
if (task.calculate_next(now + 1s))
{
tasks.push(task);
}
});
return res;
}
template<typename ClockType>
void Cron<ClockType>::get_time_until_expiry_for_tasks(std::vector<std::tuple<std::string,
std::chrono::system_clock::duration>>& status) const
{
auto now = clock.now();
status.clear();
std::for_each(tasks.get_tasks().cbegin(), tasks.get_tasks().cend(),
[&status, &now](const Task& t)
{
status.emplace_back(t.get_name(), t.time_until_expiry(now));
});
}
template<typename ClockType>
std::ostream& operator<<(std::ostream& stream, const Cron<ClockType>& c)
{
std::for_each(c.tasks.get_tasks().cbegin(), c.tasks.get_tasks().cend(),
[&stream, &c](const Task& t)
{
stream << t.get_status(c.clock.now()) << '\n';
});
return stream;
}
}

View File

@ -1,4 +1,4 @@
#include "libcron/CronClock.h" #include "CronClock.h"
#ifdef WIN32 #ifdef WIN32
#ifndef NOMINMAX #ifndef NOMINMAX

View File

@ -1,58 +1,34 @@
#include <date/date.h> #include <date/date.h>
#include "libcron/CronData.h" #include "CronData.h"
using namespace date; using namespace date;
namespace libcron namespace libcron
{ {
const constexpr Months CronData::months_with_31[NUMBER_OF_LONG_MONTHS] = { Months::January,
Months::March,
Months::May,
Months::July,
Months::August,
Months::October,
Months::December };
const std::vector<std::string> CronData::month_names{ "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" };
const std::vector<std::string> CronData::day_names{ "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" };
std::unordered_map<std::string, CronData> CronData::cache{};
CronData CronData::create(const std::string& cron_expression) CronData CronData::create(const std::string& cron_expression)
{ {
CronData c; CronData c;
auto found = cache.find(cron_expression); c.parse(cron_expression);
if (found == cache.end())
{
c.parse(cron_expression);
cache[cron_expression] = c;
}
else
{
c = found->second;
}
return c; return c;
} }
CronData::CronData()
: month_names({"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}),
day_names({"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"})
{
}
void CronData::parse(const std::string& cron_expression) void CronData::parse(const std::string& cron_expression)
{ {
// First, check for "convenience scheduling" using @yearly, @annually, // First, split on white-space. We expect six parts.
// @monthly, @weekly, @daily or @hourly. std::regex split{R"#(^\s*(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s*$)#",
std::string tmp = std::regex_replace(cron_expression, std::regex("@yearly"), "0 0 1 1 *"); std::regex_constants::ECMAScript};
tmp = std::regex_replace(tmp, std::regex("@annually"), "0 0 1 1 *");
tmp = std::regex_replace(tmp, std::regex("@monthly"), "0 0 1 * *");
tmp = std::regex_replace(tmp, std::regex("@weekly"), "0 0 * * 0");
tmp = std::regex_replace(tmp, std::regex("@daily"), "0 0 * * *");
const std::string expression = std::regex_replace(tmp, std::regex("@hourly"), "0 * * * *");
// Second, split on white-space. We expect six parts.
std::regex split{ R"#(^\s*(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s*$)#",
std::regex_constants::ECMAScript };
std::smatch match; std::smatch match;
if (std::regex_match(expression.begin(), expression.end(), match, split)) if (std::regex_match(cron_expression.begin(), cron_expression.end(), match, split))
{ {
valid = validate_numeric<Seconds>(match[1], seconds); valid = validate_numeric<Seconds>(match[1], seconds);
valid &= validate_numeric<Minutes>(match[2], minutes); valid &= validate_numeric<Minutes>(match[2], minutes);
@ -72,12 +48,13 @@ namespace libcron
std::string r = "["; std::string r = "[";
r += token; r += token;
r += "]"; r += "]";
std::regex splitter{ r, std::regex_constants::ECMAScript }; std::regex splitter{r, std::regex_constants::ECMAScript};
std::copy(std::sregex_token_iterator(s.begin(), s.end(), splitter, -1), std::copy(std::sregex_token_iterator(s.begin(), s.end(), splitter, -1),
std::sregex_token_iterator(), std::sregex_token_iterator(),
std::back_inserter(res)); std::back_inserter(res));
return res; return res;
} }
@ -113,15 +90,25 @@ namespace libcron
// Make sure that if the days contains only 31, at least one month allows that date. // Make sure that if the days contains only 31, at least one month allows that date.
if (day_of_month.size() == 1 && day_of_month.find(DayOfMonth::Last) != day_of_month.end()) if (day_of_month.size() == 1 && day_of_month.find(DayOfMonth::Last) != day_of_month.end())
{ {
res = false; std::vector<int32_t> months_with_31;
for (int32_t i = 1; i <= 12; ++i)
for (size_t i = 0; !res && i < NUMBER_OF_LONG_MONTHS; ++i)
{ {
res = months.find(months_with_31[i]) != months.end(); auto ymd = 2018_y / i / date::last;
if (unsigned(ymd.day()) == 31)
{
months_with_31.push_back(i);
}
}
res = false;
for (size_t i = 0; !res && i < months_with_31.size(); ++i)
{
res = months.find(static_cast<Months>(months_with_31[i])) != months.end();
} }
} }
} }
return res; return res;
} }
@ -135,9 +122,9 @@ namespace libcron
// '?' as the ignore flag, although it is functionally equivalent to '*'. // '?' as the ignore flag, although it is functionally equivalent to '*'.
auto check = [](const std::string& l, std::string r) auto check = [](const std::string& l, std::string r)
{ {
return l == "*" && (r != "*" || r == "?"); return l == "*" && (r != "*" || r == "?");
}; };
return (dom == "?" || dow == "?") return (dom == "?" || dow == "?")
|| check(dom, dow) || check(dom, dow)

View File

@ -4,25 +4,18 @@
#include <regex> #include <regex>
#include <string> #include <string>
#include <vector> #include <vector>
#include <unordered_map> #include "TimeTypes.h"
#include <libcron/TimeTypes.h>
namespace libcron namespace libcron
{ {
class CronData class CronData
{ {
public: public:
static const int NUMBER_OF_LONG_MONTHS = 7;
static const libcron::Months months_with_31[NUMBER_OF_LONG_MONTHS];
static CronData create(const std::string& cron_expression); static CronData create(const std::string& cron_expression);
CronData() = default; CronData();
CronData(const CronData&) = default; CronData(const CronData&) = default;
CronData& operator=(const CronData&) = default;
bool is_valid() const bool is_valid() const
{ {
return valid; return valid;
@ -68,7 +61,6 @@ namespace libcron
static bool has_any_in_range(const std::set<T>& set, uint8_t low, uint8_t high) static bool has_any_in_range(const std::set<T>& set, uint8_t low, uint8_t high)
{ {
bool found = false; bool found = false;
for (auto i = low; !found && i <= high; ++i) for (auto i = low; !found && i <= high; ++i)
{ {
found |= set.find(static_cast<T>(i)) != set.end(); found |= set.find(static_cast<T>(i)) != set.end();
@ -77,13 +69,8 @@ namespace libcron
return found; return found;
} }
template<typename T>
bool convert_from_string_range_to_number_range(const std::string& range, std::set<T>& numbers);
template<typename T>
static std::string& replace_string_name_with_numeric(std::string& s);
private: private:
void parse(const std::string& cron_expression); void parse(const std::string& cron_expression);
template<typename T> template<typename T>
@ -127,9 +114,8 @@ namespace libcron
std::set<DayOfWeek> day_of_week{}; std::set<DayOfWeek> day_of_week{};
bool valid = false; bool valid = false;
static const std::vector<std::string> month_names; std::vector<std::string> month_names;
static const std::vector<std::string> day_names; std::vector<std::string> day_names;
static std::unordered_map<std::string, CronData> cache;
template<typename T> template<typename T>
void add_full_range(std::set<T>& set); void add_full_range(std::set<T>& set);
@ -156,20 +142,20 @@ namespace libcron
for (const auto& name : names) for (const auto& name : names)
{ {
std::regex m(name, std::regex_constants::ECMAScript | std::regex_constants::icase); std::regex m(name, std::regex_constants::ECMAScript | std::regex_constants::icase);
for (size_t i = 0; i < parts.size(); ++i)
for (auto& part : parts)
{ {
std::string replaced; std::string replaced;
std::regex_replace(std::back_inserter(replaced), part.begin(), part.end(), m, std::regex_replace(std::back_inserter(replaced), parts[i].begin(), parts[i].end(), m,
std::to_string(value_of_first_name)); std::to_string(value_of_first_name));
part = replaced; parts[i] = replaced;
} }
value_of_first_name++; value_of_first_name++;
} }
return process_parts(parts, numbers); return process_parts(parts, numbers);
} }
template<typename T> template<typename T>
@ -177,9 +163,60 @@ namespace libcron
{ {
bool res = true; bool res = true;
T left;
T right;
uint8_t step_start;
uint8_t step;
for (const auto& p : parts) for (const auto& p : parts)
{ {
res &= convert_from_string_range_to_number_range(p, numbers); if (p == "*" || p == "?")
{
// We treat the ignore-character '?' the same as the full range being allowed.
add_full_range<T>(numbers);
}
else if (is_number(p))
{
res &= add_number<T>(numbers, std::stoi(p));
}
else if (get_range<T>(p, left, right))
{
// A range can be written as both 1-22 or 22-1, meaning totally different ranges.
// First case is 1...22 while 22-1 is only four hours: 22, 23, 0, 1.
if (left <= right)
{
for (auto v = value_of(left); v <= value_of(right); ++v)
{
res &= add_number(numbers, v);
}
}
else
{
// 'left' and 'right' are not in value order. First, get values between 'left' and T::Last, inclusive
for (auto v = value_of(left); v <= value_of(T::Last); ++v)
{
res &= add_number(numbers, v);
}
// Next, get values between T::First and 'right', inclusive.
for (auto v = value_of(T::First); v <= value_of(right); ++v)
{
res &= add_number(numbers, v);
}
}
}
else if (get_step<T>(p, step_start, step))
{
// Add from step_start to T::Last with a step of 'step'
for (auto v = step_start; v <= value_of(T::Last); v += step)
{
res &= add_number(numbers, v);
}
}
else
{
res = false;
}
} }
return res; return res;
@ -198,8 +235,8 @@ namespace libcron
if (std::regex_match(s.begin(), s.end(), match, range)) if (std::regex_match(s.begin(), s.end(), match, range))
{ {
auto left = std::stoi(match[1].str()); auto left = std::stoi(match[1].str().c_str());
auto right = std::stoi(match[2].str()); auto right = std::stoi(match[2].str().c_str());
if (is_within_limits<T>(left, right)) if (is_within_limits<T>(left, right))
{ {
@ -226,17 +263,16 @@ namespace libcron
if (std::regex_match(s.begin(), s.end(), match, range)) if (std::regex_match(s.begin(), s.end(), match, range))
{ {
int raw_start; int raw_start;
if(match[1].str() == "*")
if (match[1].str() == "*")
{ {
raw_start = value_of(T::First); raw_start = value_of(T::First);
} }
else else
{ {
raw_start = std::stoi(match[1].str()); raw_start = std::stoi(match[1].str().c_str());
} }
auto raw_step = std::stoi(match[2].str()); auto raw_step = std::stoi(match[2].str().c_str());
if (is_within_limits<T>(raw_start, raw_start) && raw_step > 0) if (is_within_limits<T>(raw_start, raw_start) && raw_step > 0)
{ {
@ -290,100 +326,5 @@ namespace libcron
&& is_between(high, value_of(T::First), value_of(T::Last)); && is_between(high, value_of(T::First), value_of(T::Last));
} }
template<typename T>
bool CronData::convert_from_string_range_to_number_range(const std::string& range, std::set<T>& numbers)
{
T left;
T right;
uint8_t step_start;
uint8_t step;
bool res = true;
if (range == "*" || range == "?")
{
// We treat the ignore-character '?' the same as the full range being allowed.
add_full_range<T>(numbers);
}
else if (is_number(range))
{
res = add_number<T>(numbers, std::stoi(range));
}
else if (get_range<T>(range, left, right))
{
// A range can be written as both 1-22 or 22-1, meaning totally different ranges.
// First case is 1...22 while 22-1 is only four hours: 22, 23, 0, 1.
if (left <= right)
{
for (auto v = value_of(left); v <= value_of(right); ++v)
{
res &= add_number(numbers, v);
}
}
else
{
// 'left' and 'right' are not in value order. First, get values between 'left' and T::Last, inclusive
for (auto v = value_of(left); v <= value_of(T::Last); ++v)
{
res = add_number(numbers, v);
}
// Next, get values between T::First and 'right', inclusive.
for (auto v = value_of(T::First); v <= value_of(right); ++v)
{
res = add_number(numbers, v);
}
}
}
else if (get_step<T>(range, step_start, step))
{
// Add from step_start to T::Last with a step of 'step'
for (auto v = step_start; v <= value_of(T::Last); v += step)
{
res = add_number(numbers, v);
}
}
else
{
res = false;
}
return res;
}
template<typename T>
std::string & CronData::replace_string_name_with_numeric(std::string& s)
{
auto value = static_cast<int>(T::First);
const std::vector<std::string>* name_source{};
static_assert(std::is_same<T, libcron::Months>()
|| std::is_same<T, libcron::DayOfWeek>(),
"T must be either Months or DayOfWeek");
if constexpr (std::is_same<T, libcron::Months>())
{
name_source = &month_names;
}
else
{
name_source = &day_names;
}
for (const auto& name : *name_source)
{
std::regex m(name, std::regex_constants::ECMAScript | std::regex_constants::icase);
std::string replaced;
std::regex_replace(std::back_inserter(replaced), s.begin(), s.end(), m, std::to_string(value));
s = replaced;
++value;
}
return s;
}
} }

View File

@ -1,4 +1,4 @@
#include "libcron/CronSchedule.h" #include "CronSchedule.h"
#include <tuple> #include <tuple>
using namespace std::chrono; using namespace std::chrono;
@ -46,7 +46,7 @@ namespace libcron
//Add days until the current weekday is one of the allowed weekdays //Add days until the current weekday is one of the allowed weekdays
year_month_weekday ymw = date::floor<days>(curr); year_month_weekday ymw = date::floor<days>(curr);
if (data.get_day_of_week().find(static_cast<DayOfWeek>(ymw.weekday().c_encoding())) == if (data.get_day_of_week().find(static_cast<DayOfWeek>(unsigned(ymw.weekday()))) ==
data.get_day_of_week().end()) data.get_day_of_week().end())
{ {
sys_days s = ymd; sys_days s = ymd;
@ -81,16 +81,6 @@ namespace libcron
} }
} }
// Discard fraction seconds in the calculated schedule time
// that may leftover from the argument `from`, which in turn comes from `now()`.
// Fraction seconds will potentially make the task be triggered more than 1 second late
// if the `tick()` within the same second is earlier than schedule time,
// in that the task will not trigger until the next `tick()` next second.
// By discarding fraction seconds in the scheduled time,
// the `tick()` within the same second will never be earlier than schedule time,
// and the task will trigger in that `tick()`.
curr -= curr.time_since_epoch() % seconds{1};
return std::make_tuple(max_iterations > 0, curr); return std::make_tuple(max_iterations > 0, curr);
} }
} }

View File

@ -1,17 +1,9 @@
#pragma once #pragma once
#include "libcron/CronData.h" #include "CronData.h"
#include <chrono> #include <chrono>
#if defined(_MSC_VER)
#pragma warning(push)
#pragma warning(disable:4244)
#endif
#include <date/date.h> #include <date/date.h>
#if defined(_MSC_VER) #include "DateTime.h"
#pragma warning(pop)
#endif
#include "libcron/DateTime.h"
namespace libcron namespace libcron
{ {
@ -25,8 +17,6 @@ namespace libcron
CronSchedule(const CronSchedule&) = default; CronSchedule(const CronSchedule&) = default;
CronSchedule& operator=(const CronSchedule&) = default;
std::tuple<bool, std::chrono::system_clock::time_point> std::tuple<bool, std::chrono::system_clock::time_point>
calculate_from(const std::chrono::system_clock::time_point& from) const; calculate_from(const std::chrono::system_clock::time_point& from) const;

View File

@ -1,4 +1,5 @@
#include "libcron/Task.h" #include <iostream>
#include "Task.h"
using namespace std::chrono; using namespace std::chrono;

View File

@ -1,43 +1,26 @@
#pragma once #pragma once
#include <functional> #include <functional>
#include <chrono>
#include <utility>
#include "CronData.h" #include "CronData.h"
#include "CronSchedule.h" #include "CronSchedule.h"
#include <chrono>
#include <utility>
namespace libcron namespace libcron
{ {
class TaskInformation class Task
{ {
public: public:
virtual ~TaskInformation() = default;
virtual std::chrono::system_clock::duration get_delay() const = 0;
virtual std::string get_name() const = 0;
};
class Task : public TaskInformation Task(std::string name, const CronSchedule schedule, std::function<void()> task)
{
public:
using TaskFunction = std::function<void(const TaskInformation&)>;
Task(std::string name, const CronSchedule schedule, TaskFunction task)
: name(std::move(name)), schedule(std::move(schedule)), task(std::move(task)) : name(std::move(name)), schedule(std::move(schedule)), task(std::move(task))
{ {
} }
void execute(std::chrono::system_clock::time_point now) void execute(std::chrono::system_clock::time_point now)
{ {
// Next Schedule is still the current schedule, calculate delay (actual execution - planned execution)
delay = now - next_schedule;
last_run = now; last_run = now;
task(*this); task();
}
std::chrono::system_clock::duration get_delay() const override
{
return delay;
} }
Task(const Task& other) = default; Task(const Task& other) = default;
@ -51,17 +34,12 @@ namespace libcron
return next_schedule > other.next_schedule; return next_schedule > other.next_schedule;
} }
bool operator<(const Task& other) const
{
return next_schedule < other.next_schedule;
}
bool is_expired(std::chrono::system_clock::time_point now) const; bool is_expired(std::chrono::system_clock::time_point now) const;
std::chrono::system_clock::duration std::chrono::system_clock::duration
time_until_expiry(std::chrono::system_clock::time_point now) const; time_until_expiry(std::chrono::system_clock::time_point now) const;
std::string get_name() const override std::string get_name() const
{ {
return name; return name;
} }
@ -72,29 +50,8 @@ namespace libcron
std::string name; std::string name;
CronSchedule schedule; CronSchedule schedule;
std::chrono::system_clock::time_point next_schedule; std::chrono::system_clock::time_point next_schedule;
std::chrono::system_clock::duration delay = std::chrono::seconds(-1); std::function<void()> task;
TaskFunction task;
bool valid = false; bool valid = false;
std::chrono::system_clock::time_point last_run = std::numeric_limits<std::chrono::system_clock::time_point>::min(); std::chrono::system_clock::time_point last_run = std::numeric_limits<std::chrono::system_clock::time_point>::min();
}; };
} }
inline bool operator==(const std::string &lhs, const libcron::Task &rhs)
{
return lhs == rhs.get_name();
}
inline bool operator==(const libcron::Task &lhs, const std::string &rhs)
{
return lhs.get_name() == rhs;
}
inline bool operator!=(const std::string &lhs, const libcron::Task &rhs)
{
return !(lhs == rhs);
}
inline bool operator!=(const libcron::Task &lhs, const std::string &rhs)
{
return !(lhs == rhs);
}

43
libcron/TimeTypes.h Normal file
View File

@ -0,0 +1,43 @@
#pragma once
#include <cstdint>
namespace libcron
{
enum class Seconds : int8_t
{
First = 0,
Last = 59
};
enum class Minutes : int8_t
{
First = 0,
Last = 59
};
enum class Hours : int8_t
{
First = 0,
Last = 23
};
enum class DayOfMonth : uint8_t
{
First = 1,
Last = 31
};
enum class Months : uint8_t
{
First = 1,
Last = 12
};
enum class DayOfWeek : uint8_t
{
// Sunday = 0 ... Saturday = 6
First = 0,
Last = 6,
};
}

@ -1 +1 @@
Subproject commit cac99da8dc88be719a728dc1b597b0ac307c1800 Subproject commit 38c5ca38bb73b292b72e088c31595add564d31f6

View File

@ -1,300 +0,0 @@
#pragma once
#include <string>
#include <chrono>
#include <memory>
#include <mutex>
#include <map>
#include <unordered_map>
#include <vector>
#include "Task.h"
#include "CronClock.h"
#include "TaskQueue.h"
namespace libcron
{
class NullLock
{
public:
void lock() {}
void unlock() {}
};
class Locker
{
public:
void lock() { m.lock(); }
void unlock() { m.unlock(); }
private:
std::recursive_mutex m{};
};
template<typename ClockType, typename LockType>
class Cron;
template<typename ClockType, typename LockType>
std::ostream& operator<<(std::ostream& stream, const Cron<ClockType, LockType>& c);
template<typename ClockType = libcron::LocalClock,
typename LockType = libcron::NullLock>
class Cron
{
public:
bool add_schedule(std::string name, const std::string& schedule, Task::TaskFunction work);
template<typename Schedules = std::map<std::string, std::string>>
std::tuple<bool, std::string, std::string>
add_schedule(const Schedules& name_schedule_map, Task::TaskFunction work);
void clear_schedules();
void remove_schedule(const std::string& name);
size_t count() const
{
return tasks.size();
}
// Tick is expected to be called at least once a second to prevent missing schedules.
size_t
tick()
{
return tick(clock.now());
}
size_t
tick(std::chrono::system_clock::time_point now);
std::chrono::system_clock::duration
time_until_next() const;
ClockType& get_clock()
{
return clock;
}
void recalculate_schedule()
{
for (auto& t : tasks.get_tasks())
{
using namespace std::chrono_literals;
// Ensure that next schedule is in the future
t.calculate_next(clock.now() + 1s);
}
}
void get_time_until_expiry_for_tasks(
std::vector<std::tuple<std::string, std::chrono::system_clock::duration>>& status) const;
friend std::ostream& operator<<<>(std::ostream& stream, const Cron<ClockType, LockType>& c);
private:
TaskQueue<LockType> tasks{};
ClockType clock{};
bool first_tick = true;
std::chrono::system_clock::time_point last_tick{};
};
template<typename ClockType, typename LockType>
bool Cron<ClockType, LockType>::add_schedule(std::string name, const std::string& schedule, Task::TaskFunction work)
{
auto cron = CronData::create(schedule);
bool res = cron.is_valid();
if (res)
{
tasks.lock_queue();
Task t{std::move(name), CronSchedule{cron}, work };
if (t.calculate_next(clock.now()))
{
tasks.push(t);
tasks.sort();
}
tasks.release_queue();
}
return res;
}
template<typename ClockType, typename LockType>
template<typename Schedules>
std::tuple<bool, std::string, std::string>
Cron<ClockType, LockType>::add_schedule(const Schedules& name_schedule_map, Task::TaskFunction work)
{
bool is_valid = true;
std::tuple<bool, std::string, std::string> res{false, "", ""};
std::vector<Task> tasks_to_add;
tasks_to_add.reserve(name_schedule_map.size());
for (auto it = name_schedule_map.begin(); is_valid && it != name_schedule_map.end(); ++it)
{
const auto& [name, schedule] = *it;
auto cron = CronData::create(schedule);
is_valid = cron.is_valid();
if (is_valid)
{
Task t{std::move(name), CronSchedule{cron}, work };
if (t.calculate_next(clock.now()))
{
tasks_to_add.push_back(std::move(t));
}
}
else
{
std::get<1>(res) = name;
std::get<2>(res) = schedule;
}
}
// Only add tasks and sort once if all elements in the map where valid
if (is_valid && tasks_to_add.size() > 0)
{
tasks.lock_queue();
tasks.push(tasks_to_add);
tasks.sort();
tasks.release_queue();
}
std::get<0>(res) = is_valid;
return res;
}
template<typename ClockType, typename LockType>
void Cron<ClockType, LockType>::clear_schedules()
{
tasks.clear();
}
template<typename ClockType, typename LockType>
void Cron<ClockType, LockType>::remove_schedule(const std::string& name)
{
tasks.remove(name);
}
template<typename ClockType, typename LockType>
std::chrono::system_clock::duration Cron<ClockType, LockType>::time_until_next() const
{
std::chrono::system_clock::duration d{};
if (tasks.empty())
{
d = std::numeric_limits<std::chrono::minutes>::max();
}
else
{
d = tasks.top().time_until_expiry(clock.now());
}
return d;
}
template<typename ClockType, typename LockType>
size_t Cron<ClockType, LockType>::tick(std::chrono::system_clock::time_point now)
{
tasks.lock_queue();
size_t res = 0;
if(!first_tick)
{
// Only allow time to flow if at least one second has passed since the last tick,
// either forward or backward.
auto diff = now - last_tick;
constexpr auto one_second = std::chrono::seconds{1};
if(diff < one_second && diff > -one_second)
{
now = last_tick;
}
}
if (first_tick)
{
first_tick = false;
}
else
{
// https://linux.die.net/man/8/cron
constexpr auto three_hours = std::chrono::hours{3};
auto diff = now - last_tick;
auto absolute_diff = diff > diff.zero() ? diff : -diff;
if(absolute_diff >= three_hours)
{
// Time changes of more than 3 hours are considered to be corrections to the
// clock or timezone, and the new time is used immediately.
for (auto& t : tasks.get_tasks())
{
t.calculate_next(now);
}
}
else
{
// Change of less than three hours
// If time has moved backwards: Since tasks are not rescheduled, they won't run before
// we're back at least the original point in time which prevents running tasks twice.
// If time has moved forward, tasks that would have run since last tick will be run.
}
}
last_tick = now;
if (!tasks.empty())
{
for (size_t i = 0; i < tasks.size(); i++)
{
if (tasks.at(i).is_expired(now))
{
auto& t = tasks.at(i);
t.execute(now);
using namespace std::chrono_literals;
if (!t.calculate_next(now + 1s))
{
tasks.remove(t);
}
res++;
}
}
// Only sort if at least one task was executed
if (res > 0)
{
tasks.sort();
}
}
tasks.release_queue();
return res;
}
template<typename ClockType, typename LockType>
void Cron<ClockType, LockType>::get_time_until_expiry_for_tasks(std::vector<std::tuple<std::string,
std::chrono::system_clock::duration>>& status) const
{
auto now = clock.now();
status.clear();
std::for_each(tasks.get_tasks().cbegin(), tasks.get_tasks().cend(),
[&status, &now](const Task& t)
{
status.emplace_back(t.get_name(), t.time_until_expiry(now));
});
}
template<typename ClockType, typename LockType>
std::ostream& operator<<(std::ostream& stream, const Cron<ClockType, LockType>& c)
{
std::for_each(c.tasks.get_tasks().cbegin(), c.tasks.get_tasks().cend(),
[&stream, &c](const Task& t)
{
stream << t.get_status(c.clock.now()) << '\n';
});
return stream;
}
}

View File

@ -1,100 +0,0 @@
#pragma once
#include <tuple>
#include <random>
#include <regex>
#include <functional>
#include "CronData.h"
namespace libcron
{
class CronRandomization
{
public:
std::tuple<bool, std::string> parse(const std::string& cron_schedule);
CronRandomization();
CronRandomization(const CronRandomization&) = delete;
CronRandomization & operator=(const CronRandomization &) = delete;
private:
template<typename T>
std::pair<bool, std::string> get_random_in_range(const std::string& section,
int& selected_value,
std::pair<int, int> limit = std::make_pair(-1, -1));
std::pair<int, int> day_limiter(const std::set<Months>& month);
int cap(int value, int lower, int upper);
std::regex const rand_expression{ R"#([rR]\((\d+)\-(\d+)\))#", std::regex_constants::ECMAScript };
std::random_device rd{};
std::mt19937 twister;
};
template<typename T>
std::pair<bool, std::string> CronRandomization::get_random_in_range(const std::string& section,
int& selected_value,
std::pair<int, int> limit)
{
auto res = std::make_pair(true, std::string{});
selected_value = -1;
std::smatch random_match;
if (std::regex_match(section.cbegin(), section.cend(), random_match, rand_expression))
{
// Random range, get left and right numbers.
auto left = std::stoi(random_match[1].str());
auto right = std::stoi(random_match[2].str());
if (limit.first != -1 && limit.second != -1)
{
left = cap(left, limit.first, limit.second);
right = cap(right, limit.first, limit.second);
}
libcron::CronData cd;
std::set<T> numbers;
res.first = cd.convert_from_string_range_to_number_range<T>(
std::to_string(left) + "-" + std::to_string(right), numbers);
// Remove items outside limits.
if (limit.first != -1 && limit.second != -1)
{
for (auto it = numbers.begin(); it != numbers.end(); )
{
if (CronData::value_of(*it) < limit.first || CronData::value_of(*it) > limit.second)
{
it = numbers.erase(it);
}
else
{
++it;
}
}
}
if (res.first)
{
// Generate random indexes to select one of the numbers in the range.
std::uniform_int_distribution<> dis(0, static_cast<int>(numbers.size() - 1));
// Select the random number to use as the schedule
auto it = numbers.begin();
std::advance(it, dis(twister));
selected_value = CronData::value_of(*it);
res.second = std::to_string(selected_value);
}
}
else
{
// Not random, just append input to output.
res.second = section;
}
return res;
}
}

View File

@ -1,115 +0,0 @@
#pragma once
#include <vector>
#include <map>
#include <unordered_map>
#include <vector>
#include "Task.h"
namespace libcron
{
template<typename LockType>
class TaskQueue
{
public:
const std::vector<Task>& get_tasks() const
{
return c;
}
std::vector<Task>& get_tasks()
{
return c;
}
size_t size() const noexcept
{
return c.size();
}
bool empty() const noexcept
{
return c.empty();
}
void push(Task& t)
{
c.push_back(std::move(t));
}
void push(Task&& t)
{
c.push_back(std::move(t));
}
void push(std::vector<Task>& tasks_to_insert)
{
c.reserve(c.size() + tasks_to_insert.size());
c.insert(c.end(), std::make_move_iterator(tasks_to_insert.begin()), std::make_move_iterator(tasks_to_insert.end()));
}
const Task& top() const
{
return c[0];
}
Task& at(const size_t i)
{
return c[i];
}
void sort()
{
std::sort(c.begin(), c.end(), std::less<>());
}
void clear()
{
lock.lock();
c.clear();
lock.unlock();
}
void remove(Task& to_remove)
{
auto it = std::find_if(c.begin(), c.end(), [&to_remove] (const Task& to_compare) {
return to_remove.get_name() == to_compare;
});
if (it != c.end())
{
c.erase(it);
}
}
void remove(std::string to_remove)
{
lock.lock();
auto it = std::find_if(c.begin(), c.end(), [&to_remove] (const Task& to_compare) {
return to_remove == to_compare;
});
if (it != c.end())
{
c.erase(it);
}
lock.unlock();
}
void lock_queue()
{
/* Do not allow to manipulate the Queue */
lock.lock();
}
void release_queue()
{
/* Allow Access to the Queue Manipulating-Functions */
lock.unlock();
}
private:
LockType lock;
std::vector<Task> c;
};
}

View File

@ -1,55 +0,0 @@
#pragma once
#include <cstdint>
namespace libcron
{
enum class Seconds : int8_t
{
First = 0,
Last = 59
};
enum class Minutes : int8_t
{
First = 0,
Last = 59
};
enum class Hours : int8_t
{
First = 0,
Last = 23
};
enum class DayOfMonth : uint8_t
{
First = 1,
Last = 31
};
enum class Months : uint8_t
{
First = 1,
January = First,
February,
March,
April,
May,
June,
July,
August,
September,
October,
November,
December = 12,
Last = December
};
enum class DayOfWeek : uint8_t
{
// Sunday = 0 ... Saturday = 6
First = 0,
Last = 6,
};
}

View File

@ -1,140 +0,0 @@
#include <libcron/CronRandomization.h>
#include <regex>
#include <map>
#include <array>
#include <algorithm>
#include <iterator>
#include <libcron/TimeTypes.h>
#include <libcron/CronData.h>
namespace libcron
{
CronRandomization::CronRandomization()
: twister(rd())
{
}
std::tuple<bool, std::string> CronRandomization::parse(const std::string& cron_schedule)
{
// Split on space to get each separate part, six parts expected
const std::regex split{ R"#(^\s*(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s+(.*?)\s*$)#",
std::regex_constants::ECMAScript };
std::smatch all_sections;
auto res = std::regex_match(cron_schedule.cbegin(), cron_schedule.cend(), all_sections, split);
// Replace text with numbers
std::string working_copy{};
if (res)
{
// Merge seconds, minutes, hours and day of month back together
working_copy += all_sections[1].str();
working_copy += " ";
working_copy += all_sections[2].str();
working_copy += " ";
working_copy += all_sections[3].str();
working_copy += " ";
working_copy += all_sections[4].str();
working_copy += " ";
// Replace month names
auto month = all_sections[5].str();
CronData::replace_string_name_with_numeric<libcron::Months>(month);
working_copy += " ";
working_copy += month;
// Replace day names
auto dow = all_sections[6].str();
CronData::replace_string_name_with_numeric<libcron::DayOfWeek>(dow);
working_copy += " ";
working_copy += dow;
}
std::string final_cron_schedule{};
// Split again on space
res = res && std::regex_match(working_copy.cbegin(), working_copy.cend(), all_sections, split);
if (res)
{
int selected_value = -1;
auto second = get_random_in_range<Seconds>(all_sections[1].str(), selected_value);
res = second.first;
final_cron_schedule = second.second;
auto minute = get_random_in_range<Minutes>(all_sections[2].str(), selected_value);
res &= minute.first;
final_cron_schedule += " " + minute.second;
auto hour = get_random_in_range<Hours>(all_sections[3].str(), selected_value);
res &= hour.first;
final_cron_schedule += " " + hour.second;
// Do Month before DayOfMonth to allow capping the allowed range.
auto month = get_random_in_range<Months>(all_sections[5].str(), selected_value);
res &= month.first;
std::set<Months> month_range{};
if (selected_value == -1)
{
// Month is not specific, get the range.
CronData cr;
res &= cr.convert_from_string_range_to_number_range<Months>(all_sections[5].str(), month_range);
}
else
{
month_range.emplace(static_cast<Months>(selected_value));
}
auto limits = day_limiter(month_range);
auto day_of_month = get_random_in_range<DayOfMonth>(all_sections[4].str(),
selected_value,
limits);
res &= day_of_month.first;
final_cron_schedule += " " + day_of_month.second + " " + month.second;
auto day_of_week = get_random_in_range<DayOfWeek>(all_sections[6].str(), selected_value);
res &= day_of_week.first;
final_cron_schedule += " " + day_of_week.second;
}
return { res, final_cron_schedule };
}
std::pair<int, int> CronRandomization::day_limiter(const std::set<Months>& months)
{
int max = CronData::value_of(DayOfMonth::Last);
for (auto month : months)
{
if (month == Months::February)
{
// Limit to 29 days, possibly causing delaying schedule until next leap year.
max = std::min(max, 29);
}
else if (std::find(std::begin(CronData::months_with_31),
std::end(CronData::months_with_31),
month) == std::end(CronData::months_with_31))
{
// Not among the months with 31 days
max = std::min(max, 30);
}
}
auto res = std::pair<int, int>{ CronData::value_of(DayOfMonth::First), max };
return res;
}
int CronRandomization::cap(int value, int lower, int upper)
{
return std::max(std::min(value, upper), lower);
}
}

View File

@ -1,23 +1,16 @@
cmake_minimum_required(VERSION 3.6) cmake_minimum_required(VERSION 3.6)
project(cron_test) project(cron_test)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 14)
# Deactivate Iterator-Debugging on Windows
option(LIBCRON_DEACTIVATE_ITERATOR_DEBUGGING "Build with iterator-debugging (MSVC only)." OFF)
if( MSVC ) if( MSVC )
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4")
if (LIBCRON_DEACTIVATE_ITERATOR_DEBUGGING)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_HAS_ITERATOR_DEBUGGING=0")
endif()
else() else()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic")
endif() endif()
include_directories( include_directories(
${CMAKE_CURRENT_LIST_DIR}/externals/Catch2/single_include/catch2 ${CMAKE_CURRENT_LIST_DIR}/externals/Catch2/single_include/
${CMAKE_CURRENT_LIST_DIR}/../libcron/externals/date/include ${CMAKE_CURRENT_LIST_DIR}/../libcron/externals/date/include
${CMAKE_CURRENT_LIST_DIR}/.. ${CMAKE_CURRENT_LIST_DIR}/..
) )
@ -25,20 +18,11 @@ include_directories(
add_executable( add_executable(
${PROJECT_NAME} ${PROJECT_NAME}
CronDataTest.cpp CronDataTest.cpp
CronRandomizationTest.cpp CronScheduleTest.cpp CronTest.cpp)
CronScheduleTest.cpp
CronTest.cpp)
if(NOT MSVC) target_link_libraries(${PROJECT_NAME} libcron)
target_link_libraries(${PROJECT_NAME} libcron pthread)
# Assume a modern compiler supporting uncaught_exceptions()
target_compile_definitions (${PROJECT_NAME} PRIVATE -DHAS_UNCAUGHT_EXCEPTIONS)
else()
target_link_libraries(${PROJECT_NAME} libcron)
endif()
set_target_properties(${PROJECT_NAME} PROPERTIES set_target_properties(${PROJECT_NAME} PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out" ARCHIVE_OUTPUT_DIRECTORY "${OUTPUT_LOCATION}"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out" LIBRARY_OUTPUT_DIRECTORY "${OUTPUT_LOCATION}"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out") RUNTIME_OUTPUT_DIRECTORY "${OUTPUT_LOCATION}")

View File

@ -2,8 +2,8 @@
#include <catch.hpp> #include <catch.hpp>
#include <date/date.h> #include <date/date.h>
#include <libcron/include/libcron/Cron.h> #include <libcron/Cron.h>
#include <libcron/include/libcron/CronData.h> #include <libcron/CronData.h>
using namespace libcron; using namespace libcron;
using namespace date; using namespace date;
@ -225,16 +225,3 @@ SCENARIO("Date that exist in one of the months")
{ {
REQUIRE(CronData::create("0 0 * 31 APR,MAY ?").is_valid()); REQUIRE(CronData::create("0 0 * 31 APR,MAY ?").is_valid());
} }
SCENARIO("Replacing text with numbers")
{
{
std::string s = "SUN-TUE";
REQUIRE(CronData::replace_string_name_with_numeric<libcron::DayOfWeek>(s) == "0-2");
}
{
std::string s = "JAN-DEC";
REQUIRE(CronData::replace_string_name_with_numeric<libcron::Months>(s) == "1-12");
}
}

View File

@ -1,180 +0,0 @@
#include <catch.hpp>
#include <string>
#include <unordered_map>
#include <algorithm>
#include <libcron/CronRandomization.h>
#include <libcron/Cron.h>
#include <iostream>
using namespace libcron;
const auto EXPECT_FAILURE = true;
void test(const char* const random_schedule, bool expect_failure = false)
{
libcron::CronRandomization cr;
for (int i = 0; i < 5000; ++i)
{
auto res = cr.parse(random_schedule);
auto schedule = std::get<1>(res);
Cron<> cron;
if(expect_failure)
{
// Parsing of random might succeed, but it yields an invalid schedule.
auto r = std::get<0>(res) && cron.add_schedule("validate schedule", schedule, [](auto&) {});
REQUIRE_FALSE(r);
}
else
{
REQUIRE(std::get<0>(res));
REQUIRE(cron.add_schedule("validate schedule", schedule, [](auto&) {}));
}
}
}
SCENARIO("Randomize all the things")
{
const char* random_schedule = "R(0-59) R(0-59) R(0-23) R(1-31) R(1-12) ?";
GIVEN(random_schedule)
{
THEN("Only valid schedules generated")
{
test(random_schedule);
}
}
}
SCENARIO("Randomize all the things with reverse ranges")
{
const char* random_schedule = "R(45-15) R(30-0) R(18-2) R(28-15) R(8-3) ?";
GIVEN(random_schedule)
{
THEN("Only valid schedules generated")
{
test(random_schedule);
}
}
}
SCENARIO("Randomize all the things - day of week")
{
const char* random_schedule = "R(0-59) R(0-59) R(0-23) ? R(1-12) R(0-6)";
GIVEN(random_schedule)
{
THEN("Only valid schedules generated")
{
test(random_schedule);
}
}
}
SCENARIO("Randomize all the things with reverse ranges - day of week")
{
const char* random_schedule = "R(45-15) R(30-0) R(18-2) ? R(8-3) R(4-1)";
GIVEN(random_schedule)
{
THEN("Only valid schedules generated")
{
test(random_schedule);
}
}
}
SCENARIO("Test readme examples")
{
GIVEN("0 0 R(13-20) * * ?")
{
THEN("Valid schedule generated")
{
test("0 0 R(13-20) * * ?");
}
}
GIVEN("0 0 0 ? * R(0-6)")
{
THEN("Valid schedule generated")
{
test("0 0 0 ? * R(0-6)");
}
}
GIVEN("0 R(45-15) */12 ? * *")
{
THEN("Valid schedule generated")
{
test("0 R(45-15) */12 ? * *");
}
}
}
SCENARIO("Randomization using text versions of days and months")
{
GIVEN("0 0 0 ? * R(TUE-FRI)")
{
THEN("Valid schedule generated")
{
test("0 0 0 ? * R(TUE-FRI)");
}
}
GIVEN("Valid schedule")
{
THEN("Valid schedule generated")
{
test("0 0 0 ? R(JAN-DEC) R(MON-FRI)");
}
AND_WHEN("Given 0 0 0 ? R(DEC-MAR) R(SAT-SUN)")
{
THEN("Valid schedule generated")
{
test("0 0 0 ? R(DEC-MAR) R(SAT-SUN)");
}
}
AND_THEN("Given 0 0 0 ? R(JAN-FEB) *")
{
THEN("Valid schedule generated")
{
test("0 0 0 ? R(JAN-FEB) *");
}
}
AND_THEN("Given 0 0 0 ? R(OCT-OCT) *")
{
THEN("Valid schedule generated")
{
test("0 0 0 ? R(OCT-OCT) *");
}
}
}
GIVEN("Invalid schedule")
{
THEN("No schedule generated")
{
// Day of month specified - not allowed with day of week
test("0 0 0 1 R(JAN-DEC) R(MON-SUN)", EXPECT_FAILURE);
}
AND_THEN("No schedule generated")
{
// Invalid range
test("0 0 0 ? R(JAN) *", EXPECT_FAILURE);
}
AND_THEN("No schedule generated")
{
// Days in month field
test("0 0 0 ? R(MON-TUE) *", EXPECT_FAILURE);
}
AND_THEN("No schedule generated")
{
// Month in day field
test("0 0 0 ? * R(JAN-JUN)", EXPECT_FAILURE);
}
}
}

View File

@ -1,7 +1,7 @@
#include <catch.hpp> #include <catch.hpp>
#include <chrono> #include <chrono>
#include <date/date.h> #include <date/date.h>
#include <libcron/include/libcron/Cron.h> #include <libcron/Cron.h>
#include <iostream> #include <iostream>
using namespace libcron; using namespace libcron;
@ -184,15 +184,6 @@ SCENARIO("Examples from README.md")
DT(2018_y / 03 / 1, hours{12}, minutes{13}, seconds{48}) DT(2018_y / 03 / 1, hours{12}, minutes{13}, seconds{48})
})); }));
REQUIRE(test("0 * * * * ?", DT(2018_y / 03 / 1, hours{ 12 }, minutes{ 0 }, seconds{ 10 }),
{
DT(2018_y / 03 / 1, hours{12}, minutes{1}, seconds{0}),
DT(2018_y / 03 / 1, hours{12}, minutes{2}, seconds{0}),
DT(2018_y / 03 / 1, hours{12}, minutes{3}, seconds{0}),
DT(2018_y / 03 / 1, hours{12}, minutes{4}, seconds{0})
}));
REQUIRE(test("0 0 12 * * MON-FRI", DT(2018_y / 03 / 10, hours{12}, minutes{13}, seconds{45}), REQUIRE(test("0 0 12 * * MON-FRI", DT(2018_y / 03 / 10, hours{12}, minutes{13}, seconds{45}),
{ {
DT(2018_y / 03 / 12, hours{12}), DT(2018_y / 03 / 12, hours{12}),

View File

@ -1,6 +1,5 @@
#include <catch.hpp> #include <catch.hpp>
#include <libcron/include/libcron/Cron.h> #include <libcron/Cron.h>
#include <libcron/externals/date/include/date/date.h>
#include <thread> #include <thread>
#include <iostream> #include <iostream>
@ -36,7 +35,7 @@ SCENARIO("Adding a task")
WHEN("Adding a task that runs every second") WHEN("Adding a task that runs every second")
{ {
REQUIRE(c.add_schedule("A task", "* * * * * ?", REQUIRE(c.add_schedule("A task", "* * * * * ?",
[&expired](auto&) [&expired]()
{ {
expired = true; expired = true;
}) })
@ -69,7 +68,7 @@ SCENARIO("Adding a task that expires in the future")
Cron<> c; Cron<> c;
REQUIRE(c.add_schedule("A task", REQUIRE(c.add_schedule("A task",
create_schedule_expiring_in(c.get_clock().now(), hours{0}, minutes{0}, seconds{3}), create_schedule_expiring_in(c.get_clock().now(), hours{0}, minutes{0}, seconds{3}),
[&expired](auto&) [&expired]()
{ {
expired = true; expired = true;
}) })
@ -100,50 +99,6 @@ SCENARIO("Adding a task that expires in the future")
} }
} }
SCENARIO("Get delay using Task-Information")
{
using namespace std::chrono_literals;
GIVEN("A Cron instance with one task expiring in 2 seconds, but taking 3 seconds to execute")
{
auto _2_second_expired = 0;
auto _delay = std::chrono::system_clock::duration(-1s);
Cron<> c;
REQUIRE(c.add_schedule("Two",
"*/2 * * * * ?",
[&_2_second_expired, &_delay](auto& i)
{
_2_second_expired++;
_delay = i.get_delay();
std::this_thread::sleep_for(3s);
})
);
THEN("Not yet expired")
{
REQUIRE_FALSE(_2_second_expired);
REQUIRE(_delay <= 0s);
}
WHEN("Exactly schedule task")
{
while (_2_second_expired == 0)
c.tick();
THEN("Task should have expired within a valid time")
{
REQUIRE(_2_second_expired == 1);
REQUIRE(_delay <= 1s);
}
AND_THEN("Executing another tick again, leading to execute task again immediatly, but not on time as execution has taken 3 seconds.")
{
c.tick();
REQUIRE(_2_second_expired == 2);
REQUIRE(_delay >= 1s);
}
}
}
}
SCENARIO("Task priority") SCENARIO("Task priority")
{ {
GIVEN("A Cron instance with two tasks expiring in 3 and 5 seconds, added in 'reverse' order") GIVEN("A Cron instance with two tasks expiring in 3 and 5 seconds, added in 'reverse' order")
@ -155,7 +110,7 @@ SCENARIO("Task priority")
Cron<> c; Cron<> c;
REQUIRE(c.add_schedule("Five", REQUIRE(c.add_schedule("Five",
create_schedule_expiring_in(c.get_clock().now(), hours{0}, minutes{0}, seconds{5}), create_schedule_expiring_in(c.get_clock().now(), hours{0}, minutes{0}, seconds{5}),
[&_5_second_expired](auto&) [&_5_second_expired]()
{ {
_5_second_expired++; _5_second_expired++;
}) })
@ -163,7 +118,7 @@ SCENARIO("Task priority")
REQUIRE(c.add_schedule("Three", REQUIRE(c.add_schedule("Three",
create_schedule_expiring_in(c.get_clock().now(), hours{0}, minutes{0}, seconds{3}), create_schedule_expiring_in(c.get_clock().now(), hours{0}, minutes{0}, seconds{3}),
[&_3_second_expired](auto&) [&_3_second_expired]()
{ {
_3_second_expired++; _3_second_expired++;
}) })
@ -210,7 +165,6 @@ SCENARIO("Task priority")
} }
AND_WHEN("Waiting based on the time given by the Cron instance") AND_WHEN("Waiting based on the time given by the Cron instance")
{ {
auto msec = std::chrono::duration_cast<std::chrono::milliseconds>(c.time_until_next());
std::this_thread::sleep_for(c.time_until_next()); std::this_thread::sleep_for(c.time_until_next());
c.tick(); c.tick();
@ -277,7 +231,7 @@ SCENARIO("Clock changes")
clock.set(sys_days{2018_y / 05 / 05}); clock.set(sys_days{2018_y / 05 / 05});
// Every hour // Every hour
REQUIRE(c.add_schedule("Clock change task", "0 0 * * * ?", [](auto&) REQUIRE(c.add_schedule("Clock change task", "0 0 * * * ?", []()
{ {
}) })
); );
@ -355,7 +309,7 @@ SCENARIO("Multiple ticks per second")
int run_count = 0; int run_count = 0;
// Every 10 seconds // Every 10 seconds
REQUIRE(c.add_schedule("Clock change task", "*/10 0 * * * ?", [&run_count](auto&) REQUIRE(c.add_schedule("Clock change task", "*/10 0 * * * ?", [&run_count]()
{ {
run_count++; run_count++;
}) })
@ -381,70 +335,3 @@ SCENARIO("Multiple ticks per second")
} }
} }
SCENARIO("Tasks can be added and removed from the scheduler")
{
GIVEN("A Cron instance with no task")
{
Cron<> c;
auto expired = false;
WHEN("Adding 5 tasks that runs every second")
{
REQUIRE(c.add_schedule("Task-1", "* * * * * ?",
[&expired](auto&)
{
expired = true;
})
);
REQUIRE(c.add_schedule("Task-2", "* * * * * ?",
[&expired](auto&)
{
expired = true;
})
);
REQUIRE(c.add_schedule("Task-3", "* * * * * ?",
[&expired](auto&)
{
expired = true;
})
);
REQUIRE(c.add_schedule("Task-4", "* * * * * ?",
[&expired](auto&)
{
expired = true;
})
);
REQUIRE(c.add_schedule("Task-5", "* * * * * ?",
[&expired](auto&)
{
expired = true;
})
);
THEN("Count is 5")
{
REQUIRE(c.count() == 5);
}
AND_THEN("Removing all scheduled tasks")
{
c.clear_schedules();
REQUIRE(c.count() == 0);
}
AND_THEN("Removing a task that does not exist")
{
c.remove_schedule("Task-6");
REQUIRE(c.count() == 5);
}
AND_THEN("Removing a task that does exist")
{
c.remove_schedule("Task-5");
REQUIRE(c.count() == 4);
}
}
}
}

@ -1 +0,0 @@
Subproject commit 5c88067bd339465513af4aec606bd2292f1b594a

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,62 @@
/*
* Created by Justin R. Wilson on 2/19/2017.
* Copyright 2017 Justin R. Wilson. All rights reserved.
*
* Distributed under the Boost Software License, Version 1.0. (See accompanying
* file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
*/
#ifndef TWOBLUECUBES_CATCH_REPORTER_AUTOMAKE_HPP_INCLUDED
#define TWOBLUECUBES_CATCH_REPORTER_AUTOMAKE_HPP_INCLUDED
// Don't #include any Catch headers here - we can assume they are already
// included before this header.
// This is not good practice in general but is necessary in this case so this
// file can be distributed as a single header that works with the main
// Catch single header.
namespace Catch {
struct AutomakeReporter : StreamingReporterBase<AutomakeReporter> {
AutomakeReporter( ReporterConfig const& _config )
: StreamingReporterBase( _config )
{}
~AutomakeReporter() override;
static std::string getDescription() {
return "Reports test results in the format of Automake .trs files";
}
void assertionStarting( AssertionInfo const& ) override {}
bool assertionEnded( AssertionStats const& /*_assertionStats*/ ) override { return true; }
void testCaseEnded( TestCaseStats const& _testCaseStats ) override {
// Possible values to emit are PASS, XFAIL, SKIP, FAIL, XPASS and ERROR.
stream << ":test-result: ";
if (_testCaseStats.totals.assertions.allPassed()) {
stream << "PASS";
} else if (_testCaseStats.totals.assertions.allOk()) {
stream << "XFAIL";
} else {
stream << "FAIL";
}
stream << ' ' << _testCaseStats.testInfo.name << '\n';
StreamingReporterBase::testCaseEnded( _testCaseStats );
}
void skipTest( TestCaseInfo const& testInfo ) override {
stream << ":test-result: SKIP " << testInfo.name << '\n';
}
};
#ifdef CATCH_IMPL
AutomakeReporter::~AutomakeReporter() {}
#endif
CATCH_REGISTER_REPORTER( "automake", AutomakeReporter)
} // end namespace Catch
#endif // TWOBLUECUBES_CATCH_REPORTER_AUTOMAKE_HPP_INCLUDED

View File

@ -0,0 +1,255 @@
/*
* Created by Colton Wolkins on 2015-08-15.
* Copyright 2015 Martin Moene. All rights reserved.
*
* Distributed under the Boost Software License, Version 1.0. (See accompanying
* file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
*/
#ifndef TWOBLUECUBES_CATCH_REPORTER_TAP_HPP_INCLUDED
#define TWOBLUECUBES_CATCH_REPORTER_TAP_HPP_INCLUDED
// Don't #include any Catch headers here - we can assume they are already
// included before this header.
// This is not good practice in general but is necessary in this case so this
// file can be distributed as a single header that works with the main
// Catch single header.
#include <algorithm>
namespace Catch {
struct TAPReporter : StreamingReporterBase<TAPReporter> {
using StreamingReporterBase::StreamingReporterBase;
~TAPReporter() override;
static std::string getDescription() {
return "Reports test results in TAP format, suitable for test harnesses";
}
ReporterPreferences getPreferences() const override {
ReporterPreferences prefs;
prefs.shouldRedirectStdOut = false;
return prefs;
}
void noMatchingTestCases( std::string const& spec ) override {
stream << "# No test cases matched '" << spec << "'" << std::endl;
}
void assertionStarting( AssertionInfo const& ) override {}
bool assertionEnded( AssertionStats const& _assertionStats ) override {
++counter;
AssertionPrinter printer( stream, _assertionStats, counter );
printer.print();
stream << " # " << currentTestCaseInfo->name ;
stream << std::endl;
return true;
}
void testRunEnded( TestRunStats const& _testRunStats ) override {
printTotals( _testRunStats.totals );
stream << "\n" << std::endl;
StreamingReporterBase::testRunEnded( _testRunStats );
}
private:
std::size_t counter = 0;
class AssertionPrinter {
public:
AssertionPrinter& operator= ( AssertionPrinter const& ) = delete;
AssertionPrinter( AssertionPrinter const& ) = delete;
AssertionPrinter( std::ostream& _stream, AssertionStats const& _stats, std::size_t _counter )
: stream( _stream )
, result( _stats.assertionResult )
, messages( _stats.infoMessages )
, itMessage( _stats.infoMessages.begin() )
, printInfoMessages( true )
, counter(_counter)
{}
void print() {
itMessage = messages.begin();
switch( result.getResultType() ) {
case ResultWas::Ok:
printResultType( passedString() );
printOriginalExpression();
printReconstructedExpression();
if ( ! result.hasExpression() )
printRemainingMessages( Colour::None );
else
printRemainingMessages();
break;
case ResultWas::ExpressionFailed:
if (result.isOk()) {
printResultType(passedString());
} else {
printResultType(failedString());
}
printOriginalExpression();
printReconstructedExpression();
if (result.isOk()) {
printIssue(" # TODO");
}
printRemainingMessages();
break;
case ResultWas::ThrewException:
printResultType( failedString() );
printIssue( "unexpected exception with message:" );
printMessage();
printExpressionWas();
printRemainingMessages();
break;
case ResultWas::FatalErrorCondition:
printResultType( failedString() );
printIssue( "fatal error condition with message:" );
printMessage();
printExpressionWas();
printRemainingMessages();
break;
case ResultWas::DidntThrowException:
printResultType( failedString() );
printIssue( "expected exception, got none" );
printExpressionWas();
printRemainingMessages();
break;
case ResultWas::Info:
printResultType( "info" );
printMessage();
printRemainingMessages();
break;
case ResultWas::Warning:
printResultType( "warning" );
printMessage();
printRemainingMessages();
break;
case ResultWas::ExplicitFailure:
printResultType( failedString() );
printIssue( "explicitly" );
printRemainingMessages( Colour::None );
break;
// These cases are here to prevent compiler warnings
case ResultWas::Unknown:
case ResultWas::FailureBit:
case ResultWas::Exception:
printResultType( "** internal error **" );
break;
}
}
private:
static Colour::Code dimColour() { return Colour::FileName; }
static const char* failedString() { return "not ok"; }
static const char* passedString() { return "ok"; }
void printSourceInfo() const {
Colour colourGuard( dimColour() );
stream << result.getSourceInfo() << ":";
}
void printResultType( std::string const& passOrFail ) const {
if( !passOrFail.empty() ) {
stream << passOrFail << ' ' << counter << " -";
}
}
void printIssue( std::string const& issue ) const {
stream << " " << issue;
}
void printExpressionWas() {
if( result.hasExpression() ) {
stream << ";";
{
Colour colour( dimColour() );
stream << " expression was:";
}
printOriginalExpression();
}
}
void printOriginalExpression() const {
if( result.hasExpression() ) {
stream << " " << result.getExpression();
}
}
void printReconstructedExpression() const {
if( result.hasExpandedExpression() ) {
{
Colour colour( dimColour() );
stream << " for: ";
}
std::string expr = result.getExpandedExpression();
std::replace( expr.begin(), expr.end(), '\n', ' ');
stream << expr;
}
}
void printMessage() {
if ( itMessage != messages.end() ) {
stream << " '" << itMessage->message << "'";
++itMessage;
}
}
void printRemainingMessages( Colour::Code colour = dimColour() ) {
if (itMessage == messages.end()) {
return;
}
// using messages.end() directly (or auto) yields compilation error:
std::vector<MessageInfo>::const_iterator itEnd = messages.end();
const std::size_t N = static_cast<std::size_t>( std::distance( itMessage, itEnd ) );
{
Colour colourGuard( colour );
stream << " with " << pluralise( N, "message" ) << ":";
}
for(; itMessage != itEnd; ) {
// If this assertion is a warning ignore any INFO messages
if( printInfoMessages || itMessage->type != ResultWas::Info ) {
stream << " '" << itMessage->message << "'";
if ( ++itMessage != itEnd ) {
Colour colourGuard( dimColour() );
stream << " and";
}
}
}
}
private:
std::ostream& stream;
AssertionResult const& result;
std::vector<MessageInfo> messages;
std::vector<MessageInfo>::const_iterator itMessage;
bool printInfoMessages;
std::size_t counter;
};
void printTotals( const Totals& totals ) const {
if( totals.testCases.total() == 0 ) {
stream << "1..0 # Skipped: No tests ran.";
} else {
stream << "1.." << counter;
}
}
};
#ifdef CATCH_IMPL
TAPReporter::~TAPReporter() {}
#endif
CATCH_REGISTER_REPORTER( "tap", TAPReporter )
} // end namespace Catch
#endif // TWOBLUECUBES_CATCH_REPORTER_TAP_HPP_INCLUDED

View File

@ -0,0 +1,220 @@
/*
* Created by Phil Nash on 19th December 2014
* Copyright 2014 Two Blue Cubes Ltd. All rights reserved.
*
* Distributed under the Boost Software License, Version 1.0. (See accompanying
* file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
*/
#ifndef TWOBLUECUBES_CATCH_REPORTER_TEAMCITY_HPP_INCLUDED
#define TWOBLUECUBES_CATCH_REPORTER_TEAMCITY_HPP_INCLUDED
// Don't #include any Catch headers here - we can assume they are already
// included before this header.
// This is not good practice in general but is necessary in this case so this
// file can be distributed as a single header that works with the main
// Catch single header.
#include <cstring>
#ifdef __clang__
# pragma clang diagnostic push
# pragma clang diagnostic ignored "-Wpadded"
#endif
namespace Catch {
struct TeamCityReporter : StreamingReporterBase<TeamCityReporter> {
TeamCityReporter( ReporterConfig const& _config )
: StreamingReporterBase( _config )
{
m_reporterPrefs.shouldRedirectStdOut = true;
}
static std::string escape( std::string const& str ) {
std::string escaped = str;
replaceInPlace( escaped, "|", "||" );
replaceInPlace( escaped, "'", "|'" );
replaceInPlace( escaped, "\n", "|n" );
replaceInPlace( escaped, "\r", "|r" );
replaceInPlace( escaped, "[", "|[" );
replaceInPlace( escaped, "]", "|]" );
return escaped;
}
~TeamCityReporter() override;
static std::string getDescription() {
return "Reports test results as TeamCity service messages";
}
void skipTest( TestCaseInfo const& /* testInfo */ ) override {
}
void noMatchingTestCases( std::string const& /* spec */ ) override {}
void testGroupStarting( GroupInfo const& groupInfo ) override {
StreamingReporterBase::testGroupStarting( groupInfo );
stream << "##teamcity[testSuiteStarted name='"
<< escape( groupInfo.name ) << "']\n";
}
void testGroupEnded( TestGroupStats const& testGroupStats ) override {
StreamingReporterBase::testGroupEnded( testGroupStats );
stream << "##teamcity[testSuiteFinished name='"
<< escape( testGroupStats.groupInfo.name ) << "']\n";
}
void assertionStarting( AssertionInfo const& ) override {}
bool assertionEnded( AssertionStats const& assertionStats ) override {
AssertionResult const& result = assertionStats.assertionResult;
if( !result.isOk() ) {
ReusableStringStream msg;
if( !m_headerPrintedForThisSection )
printSectionHeader( msg.get() );
m_headerPrintedForThisSection = true;
msg << result.getSourceInfo() << "\n";
switch( result.getResultType() ) {
case ResultWas::ExpressionFailed:
msg << "expression failed";
break;
case ResultWas::ThrewException:
msg << "unexpected exception";
break;
case ResultWas::FatalErrorCondition:
msg << "fatal error condition";
break;
case ResultWas::DidntThrowException:
msg << "no exception was thrown where one was expected";
break;
case ResultWas::ExplicitFailure:
msg << "explicit failure";
break;
// We shouldn't get here because of the isOk() test
case ResultWas::Ok:
case ResultWas::Info:
case ResultWas::Warning:
throw std::domain_error( "Internal error in TeamCity reporter" );
// These cases are here to prevent compiler warnings
case ResultWas::Unknown:
case ResultWas::FailureBit:
case ResultWas::Exception:
throw std::domain_error( "Not implemented" );
}
if( assertionStats.infoMessages.size() == 1 )
msg << " with message:";
if( assertionStats.infoMessages.size() > 1 )
msg << " with messages:";
for( auto const& messageInfo : assertionStats.infoMessages )
msg << "\n \"" << messageInfo.message << "\"";
if( result.hasExpression() ) {
msg <<
"\n " << result.getExpressionInMacro() << "\n"
"with expansion:\n" <<
" " << result.getExpandedExpression() << "\n";
}
if( currentTestCaseInfo->okToFail() ) {
msg << "- failure ignore as test marked as 'ok to fail'\n";
stream << "##teamcity[testIgnored"
<< " name='" << escape( currentTestCaseInfo->name )<< "'"
<< " message='" << escape( msg.str() ) << "'"
<< "]\n";
}
else {
stream << "##teamcity[testFailed"
<< " name='" << escape( currentTestCaseInfo->name )<< "'"
<< " message='" << escape( msg.str() ) << "'"
<< "]\n";
}
}
stream.flush();
return true;
}
void sectionStarting( SectionInfo const& sectionInfo ) override {
m_headerPrintedForThisSection = false;
StreamingReporterBase::sectionStarting( sectionInfo );
}
void testCaseStarting( TestCaseInfo const& testInfo ) override {
m_testTimer.start();
StreamingReporterBase::testCaseStarting( testInfo );
stream << "##teamcity[testStarted name='"
<< escape( testInfo.name ) << "']\n";
stream.flush();
}
void testCaseEnded( TestCaseStats const& testCaseStats ) override {
StreamingReporterBase::testCaseEnded( testCaseStats );
if( !testCaseStats.stdOut.empty() )
stream << "##teamcity[testStdOut name='"
<< escape( testCaseStats.testInfo.name )
<< "' out='" << escape( testCaseStats.stdOut ) << "']\n";
if( !testCaseStats.stdErr.empty() )
stream << "##teamcity[testStdErr name='"
<< escape( testCaseStats.testInfo.name )
<< "' out='" << escape( testCaseStats.stdErr ) << "']\n";
stream << "##teamcity[testFinished name='"
<< escape( testCaseStats.testInfo.name ) << "' duration='"
<< m_testTimer.getElapsedMilliseconds() << "']\n";
stream.flush();
}
private:
void printSectionHeader( std::ostream& os ) {
assert( !m_sectionStack.empty() );
if( m_sectionStack.size() > 1 ) {
os << getLineOfChars<'-'>() << "\n";
std::vector<SectionInfo>::const_iterator
it = m_sectionStack.begin()+1, // Skip first section (test case)
itEnd = m_sectionStack.end();
for( ; it != itEnd; ++it )
printHeaderString( os, it->name );
os << getLineOfChars<'-'>() << "\n";
}
SourceLineInfo lineInfo = m_sectionStack.front().lineInfo;
if( !lineInfo.empty() )
os << lineInfo << "\n";
os << getLineOfChars<'.'>() << "\n\n";
}
// if string has a : in first line will set indent to follow it on
// subsequent lines
static void printHeaderString( std::ostream& os, std::string const& _string, std::size_t indent = 0 ) {
std::size_t i = _string.find( ": " );
if( i != std::string::npos )
i+=2;
else
i = 0;
os << Column( _string )
.indent( indent+i)
.initialIndent( indent ) << "\n";
}
private:
bool m_headerPrintedForThisSection = false;
Timer m_testTimer;
};
#ifdef CATCH_IMPL
TeamCityReporter::~TeamCityReporter() {}
#endif
CATCH_REGISTER_REPORTER( "teamcity", TeamCityReporter )
} // end namespace Catch
#ifdef __clang__
# pragma clang diagnostic pop
#endif
#endif // TWOBLUECUBES_CATCH_REPORTER_TEAMCITY_HPP_INCLUDED

View File

@ -1,214 +0,0 @@
# Uncrustify-0.67-87-d75a44aa9
newlines = lf
input_tab_size = 4
output_tab_size = 4
string_replace_tab_chars = true
utf8_bom = remove
sp_arith = force
sp_arith_additive = force
sp_assign = force
sp_cpp_lambda_assign = remove
sp_cpp_lambda_paren = remove
sp_assign_default = force
sp_after_assign = force
sp_enum_paren = force
sp_enum_assign = force
sp_enum_before_assign = force
sp_enum_after_assign = force
sp_pp_stringify = remove
sp_before_pp_stringify = remove
sp_bool = force
sp_compare = force
sp_inside_paren = remove
sp_paren_paren = remove
sp_before_ptr_star = remove
sp_before_unnamed_ptr_star = remove
sp_between_ptr_star = remove
sp_after_ptr_star = force
sp_after_ptr_block_caret = remove
sp_after_ptr_star_qualifier = force
sp_after_ptr_star_func = force
sp_ptr_star_paren = force
sp_before_ptr_star_func = force
sp_before_byref = remove
sp_before_unnamed_byref = remove
sp_after_byref = force
sp_after_byref_func = force
sp_before_byref_func = force
sp_before_angle = remove
sp_inside_angle = remove
sp_angle_colon = force
sp_after_angle = force
sp_angle_paren_empty = remove
sp_angle_word = force
sp_angle_shift = remove
sp_permit_cpp11_shift = true
sp_before_sparen = force
sp_inside_sparen = remove
sp_inside_sparen_close = remove
sp_inside_sparen_open = remove
sp_after_sparen = force
sp_sparen_brace = force
sp_special_semi = remove
sp_before_semi_for = remove
sp_before_semi_for_empty = remove
sp_after_semi = remove
sp_after_semi_for_empty = force
sp_after_comma = force
sp_before_ellipsis = remove
sp_after_class_colon = force
sp_before_class_colon = force
sp_after_constr_colon = force
sp_before_constr_colon = force
sp_after_operator = remove
sp_after_operator_sym = remove
sp_after_cast = remove
sp_inside_paren_cast = remove
sp_cpp_cast_paren = remove
sp_sizeof_paren = remove
sp_inside_braces_enum = force
sp_inside_braces_struct = force
sp_after_type_brace_init_lst_open = force
sp_before_type_brace_init_lst_close = force
sp_inside_type_brace_init_lst = force
sp_inside_braces_empty = remove
sp_type_func = force
sp_type_brace_init_lst = remove
sp_func_proto_paren = remove
sp_func_proto_paren_empty = remove
sp_func_def_paren = remove
sp_inside_tparen = remove
sp_after_tparen_close = remove
sp_square_fparen = remove
sp_fparen_brace = force
sp_fparen_dbrace = force
sp_func_call_paren = remove
sp_func_class_paren_empty = remove
sp_return_paren = remove
sp_attribute_paren = remove
sp_defined_paren = remove
sp_throw_paren = remove
sp_after_throw = force
sp_catch_paren = force
sp_oc_catch_paren = force
sp_else_brace = force
sp_brace_else = force
sp_before_dc = remove
sp_after_dc = remove
sp_before_nl_cont = force
sp_cond_question = force
sp_after_new = force
sp_between_new_paren = remove
sp_inside_newop_paren = force
sp_inside_newop_paren_open = remove
sp_inside_newop_paren_close = remove
indent_columns = 4
indent_with_tabs = 0
indent_align_string = true
indent_namespace = true
indent_namespace_level = 4
indent_class = true
indent_constr_colon = true
indent_ctor_init = 4
indent_access_spec = 0
indent_access_spec_body = true
indent_cpp_lambda_body = true
indent_cpp_lambda_only_once = true
nl_assign_leave_one_liners = true
nl_class_leave_one_liners = true
nl_enum_leave_one_liners = true
nl_getset_leave_one_liners = true
nl_func_leave_one_liners = true
nl_cpp_lambda_leave_one_liners = true
nl_start_of_file = remove
nl_end_of_file = force
nl_end_of_file_min = 1
nl_enum_brace = force
nl_enum_class = remove
nl_enum_class_identifier = remove
nl_if_brace = force
nl_brace_else = force
nl_elseif_brace = force
nl_else_brace = force
nl_else_if = remove
nl_before_if_closing_paren = remove
nl_brace_finally = force
nl_finally_brace = force
nl_try_brace = force
nl_for_brace = force
nl_catch_brace = force
nl_while_brace = force
nl_do_brace = force
nl_brace_while = force
nl_enum_own_lines = force
nl_func_type_name = remove
nl_func_decl_empty = remove
nl_func_def_empty = remove
nl_func_call_empty = remove
nl_return_expr = remove
nl_after_semicolon = true
nl_after_brace_close = true
nl_before_if = force
nl_after_if = force
nl_before_for = force
nl_after_for = force
nl_before_while = force
nl_after_while = force
nl_before_switch = force
nl_after_switch = force
nl_before_synchronized = force
nl_after_synchronized = force
nl_before_do = force
nl_after_do = force
nl_max = 2
nl_after_func_proto = 2
nl_after_func_proto_group = 2
nl_after_func_class_proto = 2
nl_after_func_class_proto_group = 2
nl_before_func_body_def = 2
nl_before_func_body_proto = 2
nl_after_func_body = 2
nl_after_func_body_class = 2
nl_after_func_body_one_liner = 1
nl_before_block_comment = 2
nl_before_c_comment = 2
nl_before_cpp_comment = 2
nl_after_multiline_comment = true
nl_after_label_colon = true
nl_after_struct = 2
nl_before_class = 2
nl_after_class = 2
nl_before_access_spec = 1
nl_after_access_spec = 1
nl_comment_func_def = 1
nl_after_try_catch_finally = 2
nl_around_cs_property = 2
nl_between_get_set = 2
eat_blanks_after_open_brace = true
eat_blanks_before_close_brace = true
nl_before_return = true
pos_arith = lead
code_width = 120
ls_for_split_full = true
ls_func_split_full = true
cmt_width = 120
cmt_reflow_mode = 2
cmt_indent_multi = false
cmt_c_group = true
cmt_sp_after_star_cont = 1
mod_full_brace_do = force
mod_full_brace_for = force
mod_full_brace_function = force
mod_full_brace_if = force
mod_full_brace_nl_block_rem_mlcond = true
mod_full_brace_while = force
mod_full_brace_using = force
mod_paren_on_return = remove
mod_remove_extra_semicolon = true
mod_sort_using = true
mod_case_brace = force
mod_remove_empty_return = true
pp_ignore_define_body = true
use_indent_func_call_param = false
# option(s) with 'not default' value: 209
#