mirror of
https://github.com/PerMalmberg/libcron.git
synced 2025-04-22 08:23:04 -05:00
Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
aa3d4368d5 | ||
|
7c7d290792 | ||
|
41f238ceb0 | ||
|
0dd9df49d7 | ||
|
5f8ecc9690 | ||
|
5c8de082c1 | ||
|
a3b892a24a | ||
|
d4679b7c3c | ||
|
e91a51afc1 | ||
|
b0046755bd | ||
|
9edb758ca8 | ||
|
f3fddf5f19 | ||
|
7ef39558a1 | ||
|
76da315c13 | ||
|
440f5099ba |
21
.github/workflows/ci.yml
vendored
Normal file
21
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
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
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +1,6 @@
|
||||
[submodule "libcron/externals/date"]
|
||||
path = libcron/externals/date
|
||||
url = https://github.com/HowardHinnant/date.git
|
||||
[submodule "test/externals/Catch2"]
|
||||
path = test/externals/Catch2
|
||||
url = https://github.com/catchorg/Catch2.git
|
||||
|
@ -1,7 +1,12 @@
|
||||
cmake_minimum_required(VERSION 3.6)
|
||||
|
||||
project(top)
|
||||
add_subdirectory(libcron)
|
||||
add_subdirectory(test)
|
||||
|
||||
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)
|
||||
|
||||
|
147
README.md
147
README.md
@ -1,7 +1,131 @@
|
||||
# libcron
|
||||
A C++ scheduling library using cron formatting.
|
||||
|
||||
# Local time vs UTC
|
||||
# Using the Scheduler
|
||||
|
||||
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
|
||||
uses a `LocalClock` by default which offsets `system_clock::now()` by the current UTC-offset. If you wish to work in
|
||||
@ -55,6 +179,7 @@ Each part is separated by one or more whitespaces. It is thus important to keep
|
||||
* Invalid:
|
||||
* 0, 3, 40-50 * * * * ?
|
||||
|
||||
|
||||
`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.
|
||||
|
||||
@ -63,10 +188,29 @@ the '?'-character to ensure that it is not possible to specify a statement which
|
||||
|Expression | Meaning
|
||||
| --- | --- |
|
||||
| * * * * * ? | Every second
|
||||
| 0 * * * * ? | Every minute
|
||||
| 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 ? * * | Every twelve hours
|
||||
| @hourly | Every hour
|
||||
|
||||
Note that the expression formatting has a part for seconds and the day of week.
|
||||
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
|
||||
@ -86,4 +230,3 @@ when using randomization, i.e. mutual exclusiveness and no extra spaces.
|
||||
# Used Third party libraries
|
||||
|
||||
Howard Hinnant's [date libraries](https://github.com/HowardHinnant/date/)
|
||||
|
||||
|
@ -3,8 +3,15 @@ project(libcron)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
|
||||
# Deactivate Iterator-Debugging on Windows
|
||||
option(LIBCRON_DEACTIVATE_ITERATOR_DEBUGGING "Build with iterator-debugging (MSVC only)." OFF)
|
||||
|
||||
if( MSVC )
|
||||
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()
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic")
|
||||
endif()
|
||||
@ -28,7 +35,12 @@ 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
|
||||
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out/${CMAKE_BUILD_TYPE}"
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out/${CMAKE_BUILD_TYPE}"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out/${CMAKE_BUILD_TYPE}")
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out/${CMAKE_BUILD_TYPE}")
|
||||
|
2
libcron/externals/date
vendored
2
libcron/externals/date
vendored
@ -1 +1 @@
|
||||
Subproject commit e7e1482087f58913b80a20b04d5c58d9d6d90155
|
||||
Subproject commit cac99da8dc88be719a728dc1b597b0ac307c1800
|
@ -2,25 +2,51 @@
|
||||
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
#include <queue>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include "Task.h"
|
||||
#include "CronClock.h"
|
||||
#include "TaskQueue.h"
|
||||
|
||||
namespace libcron
|
||||
{
|
||||
template<typename ClockType>
|
||||
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>
|
||||
std::ostream& operator<<(std::ostream& stream, const Cron<ClockType>& c);
|
||||
template<typename ClockType, typename LockType>
|
||||
std::ostream& operator<<(std::ostream& stream, const Cron<ClockType, LockType>& c);
|
||||
|
||||
template<typename ClockType = libcron::LocalClock>
|
||||
template<typename ClockType = libcron::LocalClock,
|
||||
typename LockType = libcron::NullLock>
|
||||
class Cron
|
||||
{
|
||||
public:
|
||||
|
||||
bool add_schedule(std::string name, const std::string& schedule, std::function<void()> work);
|
||||
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
|
||||
{
|
||||
@ -45,55 +71,106 @@ namespace libcron
|
||||
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>& c);
|
||||
friend std::ostream& operator<<<>(std::ostream& stream, const Cron<ClockType, LockType>& 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{};
|
||||
TaskQueue<LockType> 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)
|
||||
|
||||
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)
|
||||
{
|
||||
Task t{std::move(name), CronSchedule{cron}, std::move(work)};
|
||||
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>
|
||||
std::chrono::system_clock::duration Cron<ClockType>::time_until_next() const
|
||||
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())
|
||||
@ -108,9 +185,10 @@ namespace libcron
|
||||
return d;
|
||||
}
|
||||
|
||||
template<typename ClockType>
|
||||
size_t Cron<ClockType>::tick(std::chrono::system_clock::time_point now)
|
||||
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)
|
||||
@ -164,36 +242,38 @@ namespace libcron
|
||||
|
||||
last_tick = now;
|
||||
|
||||
std::vector<Task> executed{};
|
||||
|
||||
while (!tasks.empty()
|
||||
&& tasks.top().is_expired(now))
|
||||
if (!tasks.empty())
|
||||
{
|
||||
executed.push_back(tasks.top());
|
||||
tasks.pop();
|
||||
auto& t = executed[executed.size() - 1];
|
||||
t.execute(now);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
tasks.release_queue();
|
||||
return res;
|
||||
}
|
||||
|
||||
template<typename ClockType>
|
||||
void Cron<ClockType>::get_time_until_expiry_for_tasks(std::vector<std::tuple<std::string,
|
||||
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();
|
||||
@ -206,8 +286,8 @@ namespace libcron
|
||||
});
|
||||
}
|
||||
|
||||
template<typename ClockType>
|
||||
std::ostream& operator<<(std::ostream& stream, const Cron<ClockType>& c)
|
||||
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)
|
||||
@ -217,4 +297,4 @@ namespace libcron
|
||||
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,4 +39,4 @@ namespace libcron
|
||||
|
||||
std::chrono::seconds utc_offset(std::chrono::system_clock::time_point now) const override;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
#include <regex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <libcron/TimeTypes.h>
|
||||
|
||||
namespace libcron
|
||||
@ -20,6 +21,8 @@ namespace libcron
|
||||
|
||||
CronData(const CronData&) = default;
|
||||
|
||||
CronData& operator=(const CronData&) = default;
|
||||
|
||||
bool is_valid() const
|
||||
{
|
||||
return valid;
|
||||
@ -126,6 +129,7 @@ namespace libcron
|
||||
|
||||
static const std::vector<std::string> month_names;
|
||||
static const std::vector<std::string> day_names;
|
||||
static std::unordered_map<std::string, CronData> cache;
|
||||
|
||||
template<typename T>
|
||||
void add_full_range(std::set<T>& set);
|
||||
|
@ -2,12 +2,12 @@
|
||||
|
||||
#include "libcron/CronData.h"
|
||||
#include <chrono>
|
||||
#ifdef _WIN32
|
||||
#if defined(_MSC_VER)
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable:4244)
|
||||
#endif
|
||||
#include <date/date.h>
|
||||
#ifdef _WIN32
|
||||
#if defined(_MSC_VER)
|
||||
#pragma warning(pop)
|
||||
#endif
|
||||
|
||||
@ -25,6 +25,8 @@ namespace libcron
|
||||
|
||||
CronSchedule(const CronSchedule&) = default;
|
||||
|
||||
CronSchedule& operator=(const CronSchedule&) = default;
|
||||
|
||||
std::tuple<bool, std::chrono::system_clock::time_point>
|
||||
calculate_from(const std::chrono::system_clock::time_point& from) const;
|
||||
|
||||
@ -51,4 +53,4 @@ namespace libcron
|
||||
CronData data;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include "CronData.h"
|
||||
#include "CronSchedule.h"
|
||||
#include <chrono>
|
||||
#include <utility>
|
||||
#include "CronData.h"
|
||||
#include "CronSchedule.h"
|
||||
|
||||
namespace libcron
|
||||
{
|
||||
class Task
|
||||
class TaskInformation
|
||||
{
|
||||
public:
|
||||
virtual ~TaskInformation() = default;
|
||||
virtual std::chrono::system_clock::duration get_delay() const = 0;
|
||||
virtual std::string get_name() const = 0;
|
||||
};
|
||||
|
||||
Task(std::string name, const CronSchedule schedule, std::function<void()> task)
|
||||
class Task : public TaskInformation
|
||||
{
|
||||
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))
|
||||
{
|
||||
}
|
||||
|
||||
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;
|
||||
task();
|
||||
task(*this);
|
||||
}
|
||||
|
||||
std::chrono::system_clock::duration get_delay() const override
|
||||
{
|
||||
return delay;
|
||||
}
|
||||
|
||||
Task(const Task& other) = default;
|
||||
@ -34,12 +51,17 @@ namespace libcron
|
||||
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;
|
||||
|
||||
std::chrono::system_clock::duration
|
||||
time_until_expiry(std::chrono::system_clock::time_point now) const;
|
||||
|
||||
std::string get_name() const
|
||||
std::string get_name() const override
|
||||
{
|
||||
return name;
|
||||
}
|
||||
@ -50,8 +72,29 @@ namespace libcron
|
||||
std::string name;
|
||||
CronSchedule schedule;
|
||||
std::chrono::system_clock::time_point next_schedule;
|
||||
std::function<void()> task;
|
||||
std::chrono::system_clock::duration delay = std::chrono::seconds(-1);
|
||||
TaskFunction task;
|
||||
bool valid = false;
|
||||
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);
|
||||
}
|
||||
|
115
libcron/include/libcron/TaskQueue.h
Normal file
115
libcron/include/libcron/TaskQueue.h
Normal file
@ -0,0 +1,115 @@
|
||||
#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;
|
||||
};
|
||||
}
|
@ -15,24 +15,44 @@ namespace libcron
|
||||
|
||||
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 c;
|
||||
c.parse(cron_expression);
|
||||
auto found = cache.find(cron_expression);
|
||||
|
||||
if (found == cache.end())
|
||||
{
|
||||
c.parse(cron_expression);
|
||||
cache[cron_expression] = c;
|
||||
}
|
||||
else
|
||||
{
|
||||
c = found->second;
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
void CronData::parse(const std::string& cron_expression)
|
||||
{
|
||||
// First, split on white-space. We expect six parts.
|
||||
// First, check for "convenience scheduling" using @yearly, @annually,
|
||||
// @monthly, @weekly, @daily or @hourly.
|
||||
std::string tmp = std::regex_replace(cron_expression, std::regex("@yearly"), "0 0 1 1 *");
|
||||
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;
|
||||
|
||||
if (std::regex_match(cron_expression.begin(), cron_expression.end(), match, split))
|
||||
if (std::regex_match(expression.begin(), expression.end(), match, split))
|
||||
{
|
||||
valid = validate_numeric<Seconds>(match[1], seconds);
|
||||
valid &= validate_numeric<Minutes>(match[2], minutes);
|
||||
|
@ -46,7 +46,7 @@ namespace libcron
|
||||
//Add days until the current weekday is one of the allowed weekdays
|
||||
year_month_weekday ymw = date::floor<days>(curr);
|
||||
|
||||
if (data.get_day_of_week().find(static_cast<DayOfWeek>(unsigned(ymw.weekday()))) ==
|
||||
if (data.get_day_of_week().find(static_cast<DayOfWeek>(ymw.weekday().c_encoding())) ==
|
||||
data.get_day_of_week().end())
|
||||
{
|
||||
sys_days s = ymd;
|
||||
@ -81,6 +81,16 @@ 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);
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
#include <iostream>
|
||||
#include "libcron/Task.h"
|
||||
|
||||
using namespace std::chrono;
|
||||
|
@ -3,14 +3,21 @@ project(cron_test)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
|
||||
# Deactivate Iterator-Debugging on Windows
|
||||
option(LIBCRON_DEACTIVATE_ITERATOR_DEBUGGING "Build with iterator-debugging (MSVC only)." OFF)
|
||||
|
||||
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()
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic")
|
||||
endif()
|
||||
|
||||
include_directories(
|
||||
${CMAKE_CURRENT_LIST_DIR}/externals/Catch2/single_include/
|
||||
${CMAKE_CURRENT_LIST_DIR}/externals/Catch2/single_include/catch2
|
||||
${CMAKE_CURRENT_LIST_DIR}/../libcron/externals/date/include
|
||||
${CMAKE_CURRENT_LIST_DIR}/..
|
||||
)
|
||||
@ -19,12 +26,19 @@ add_executable(
|
||||
${PROJECT_NAME}
|
||||
CronDataTest.cpp
|
||||
CronRandomizationTest.cpp
|
||||
CronScheduleTest.cpp
|
||||
CronTest.cpp)
|
||||
CronScheduleTest.cpp
|
||||
CronTest.cpp)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME} libcron)
|
||||
if(NOT MSVC)
|
||||
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
|
||||
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out"
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out")
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/out")
|
||||
|
@ -23,13 +23,13 @@ void test(const char* const random_schedule, bool expect_failure = false)
|
||||
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 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, []() {}));
|
||||
REQUIRE(cron.add_schedule("validate schedule", schedule, [](auto&) {}));
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -184,6 +184,15 @@ SCENARIO("Examples from README.md")
|
||||
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}),
|
||||
{
|
||||
DT(2018_y / 03 / 12, hours{12}),
|
||||
@ -212,4 +221,4 @@ SCENARIO("Examples from README.md")
|
||||
SCENARIO("Unable to calculate time point")
|
||||
{
|
||||
REQUIRE_FALSE(test( "0 0 * 31 FEB *", DT(2021_y / 1 / 1), DT(2022_y / 1 / 1)));
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
#include <catch.hpp>
|
||||
#include <libcron/include/libcron/Cron.h>
|
||||
#include <libcron/externals/date/include/date/date.h>
|
||||
#include <thread>
|
||||
#include <iostream>
|
||||
|
||||
@ -35,7 +36,7 @@ SCENARIO("Adding a task")
|
||||
WHEN("Adding a task that runs every second")
|
||||
{
|
||||
REQUIRE(c.add_schedule("A task", "* * * * * ?",
|
||||
[&expired]()
|
||||
[&expired](auto&)
|
||||
{
|
||||
expired = true;
|
||||
})
|
||||
@ -68,7 +69,7 @@ SCENARIO("Adding a task that expires in the future")
|
||||
Cron<> c;
|
||||
REQUIRE(c.add_schedule("A task",
|
||||
create_schedule_expiring_in(c.get_clock().now(), hours{0}, minutes{0}, seconds{3}),
|
||||
[&expired]()
|
||||
[&expired](auto&)
|
||||
{
|
||||
expired = true;
|
||||
})
|
||||
@ -99,6 +100,50 @@ 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")
|
||||
{
|
||||
GIVEN("A Cron instance with two tasks expiring in 3 and 5 seconds, added in 'reverse' order")
|
||||
@ -110,7 +155,7 @@ SCENARIO("Task priority")
|
||||
Cron<> c;
|
||||
REQUIRE(c.add_schedule("Five",
|
||||
create_schedule_expiring_in(c.get_clock().now(), hours{0}, minutes{0}, seconds{5}),
|
||||
[&_5_second_expired]()
|
||||
[&_5_second_expired](auto&)
|
||||
{
|
||||
_5_second_expired++;
|
||||
})
|
||||
@ -118,7 +163,7 @@ SCENARIO("Task priority")
|
||||
|
||||
REQUIRE(c.add_schedule("Three",
|
||||
create_schedule_expiring_in(c.get_clock().now(), hours{0}, minutes{0}, seconds{3}),
|
||||
[&_3_second_expired]()
|
||||
[&_3_second_expired](auto&)
|
||||
{
|
||||
_3_second_expired++;
|
||||
})
|
||||
@ -165,6 +210,7 @@ SCENARIO("Task priority")
|
||||
}
|
||||
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());
|
||||
c.tick();
|
||||
|
||||
@ -231,7 +277,7 @@ SCENARIO("Clock changes")
|
||||
clock.set(sys_days{2018_y / 05 / 05});
|
||||
|
||||
// Every hour
|
||||
REQUIRE(c.add_schedule("Clock change task", "0 0 * * * ?", []()
|
||||
REQUIRE(c.add_schedule("Clock change task", "0 0 * * * ?", [](auto&)
|
||||
{
|
||||
})
|
||||
);
|
||||
@ -309,7 +355,7 @@ SCENARIO("Multiple ticks per second")
|
||||
int run_count = 0;
|
||||
|
||||
// Every 10 seconds
|
||||
REQUIRE(c.add_schedule("Clock change task", "*/10 0 * * * ?", [&run_count]()
|
||||
REQUIRE(c.add_schedule("Clock change task", "*/10 0 * * * ?", [&run_count](auto&)
|
||||
{
|
||||
run_count++;
|
||||
})
|
||||
@ -334,4 +380,71 @@ 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
test/externals/Catch2
vendored
Submodule
1
test/externals/Catch2
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 5c88067bd339465513af4aec606bd2292f1b594a
|
12851
test/externals/Catch2/single_include/catch.hpp
vendored
12851
test/externals/Catch2/single_include/catch.hpp
vendored
File diff suppressed because it is too large
Load Diff
@ -1,62 +0,0 @@
|
||||
/*
|
||||
* 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
|
@ -1,255 +0,0 @@
|
||||
/*
|
||||
* 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
|
@ -1,220 +0,0 @@
|
||||
/*
|
||||
* 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
|
Loading…
x
Reference in New Issue
Block a user