[feature] Add the option to specify function per task
This commit is contained in:
parent
17476be23c
commit
1e06c8c62b
11 changed files with 237 additions and 92 deletions
|
@ -3,8 +3,7 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"hotalert/logging"
|
"hotalert/logging"
|
||||||
"hotalert/task"
|
"hotalert/task/executor"
|
||||||
"hotalert/task/target"
|
|
||||||
"hotalert/workload"
|
"hotalert/workload"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -29,7 +28,7 @@ var fileCmd = &cobra.Command{
|
||||||
}
|
}
|
||||||
|
|
||||||
var waitGroup = sync.WaitGroup{}
|
var waitGroup = sync.WaitGroup{}
|
||||||
var defaultExecutor = task.NewDefaultExecutor(target.ScrapeWebTask)
|
var defaultExecutor = executor.NewDefaultExecutor()
|
||||||
taskResultChan := defaultExecutor.Start()
|
taskResultChan := defaultExecutor.Start()
|
||||||
defer defaultExecutor.Shutdown()
|
defer defaultExecutor.Shutdown()
|
||||||
|
|
||||||
|
|
14
readme.md
14
readme.md
|
@ -5,7 +5,7 @@ Hotalert is a command line tool that for task execution and configuration. Tasks
|
||||||
in yaml files and the program parses the files, executes the tasks and emits alerts when the tasks conditions are met.
|
in yaml files and the program parses the files, executes the tasks and emits alerts when the tasks conditions are met.
|
||||||
|
|
||||||
For example if you want to send a notification to your mobile phone when a keyword is found on a website you can use the
|
For example if you want to send a notification to your mobile phone when a keyword is found on a website you can use the
|
||||||
`webhook_discord` alerter and the `scrape_web` task.
|
`webhook_discord` alerter and the `web_scrape` task function.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ tasks:
|
||||||
keywords: ["Episode 10", "Episode 11"]
|
keywords: ["Episode 10", "Episode 11"]
|
||||||
timeout: 10
|
timeout: 10
|
||||||
alerter: "webhook_discord"
|
alerter: "webhook_discord"
|
||||||
task: "scrape_web"
|
function: "web_scrape"
|
||||||
alerts:
|
alerts:
|
||||||
webhook_discord:
|
webhook_discord:
|
||||||
webhook: https://discord.com/api/webhooks/[...]
|
webhook: https://discord.com/api/webhooks/[...]
|
||||||
|
@ -24,6 +24,16 @@ alerts:
|
||||||
message: "Hi, the keyword(s) `$keywords` was found on [...]"
|
message: "Hi, the keyword(s) `$keywords` was found on [...]"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Available task functions
|
||||||
|
|
||||||
|
#### web_scrape
|
||||||
|
|
||||||
|
The web_scrape task scrapes a web page by issuing a GET request and parses the response to look for keywords.
|
||||||
|
|
||||||
|
**Options**:
|
||||||
|
- url (string) - The url to scrape.
|
||||||
|
- keywords (array[string]) - A list of keyword strings to look for.
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
To build the program for Linux under Linux use the following command:
|
To build the program for Linux under Linux use the following command:
|
||||||
|
|
|
@ -1,50 +1,65 @@
|
||||||
package task
|
package executor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hotalert/logging"
|
||||||
|
"hotalert/task"
|
||||||
|
"hotalert/task/functions"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Executor is an interface for implementing task executors.
|
// Executor is an interface for implementing task executors.
|
||||||
type Executor interface {
|
type Executor interface {
|
||||||
// AddTask adds a task to the task executor.
|
// AddTask adds a task to the task executor.
|
||||||
AddTask(task *Task)
|
AddTask(task *task.Task)
|
||||||
// Start starts the scrapper and returns a Result receive-only channel.
|
// Start starts the scrapper and returns a Result receive-only channel.
|
||||||
Start() <-chan *Result
|
Start() <-chan *task.Result
|
||||||
// Shutdown shuts down the scrapper. It will block until the Executor was shut down.
|
// Shutdown shuts down the scrapper. It will block until the Executor was shut down.
|
||||||
Shutdown()
|
Shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecutionFn is a type definition for a function that executes the task and returns an error.
|
// ExecutionFunc is a type definition for a function that executes the task and returns an error.
|
||||||
type ExecutionFn func(task *Task) error
|
type ExecutionFunc func(task *task.Task) error
|
||||||
|
|
||||||
// DefaultExecutor is a TaskExecutor with the default implementation.
|
// DefaultExecutor is a TaskExecutor with the default implementation.
|
||||||
// The tasks are executed directly on the machine.
|
// The tasks are executed directly on the machine.
|
||||||
type DefaultExecutor struct {
|
type DefaultExecutor struct {
|
||||||
// TaskExecutionFn is the function that executes the task.
|
|
||||||
TaskExecutionFn ExecutionFn
|
|
||||||
// workerGroup is a waiting group for worker goroutines.
|
// workerGroup is a waiting group for worker goroutines.
|
||||||
workerGroup *sync.WaitGroup
|
workerGroup *sync.WaitGroup
|
||||||
// numberOfWorkerGoroutines is the number of working goroutines.
|
// numberOfWorkerGoroutines is the number of working goroutines.
|
||||||
numberOfWorkerGoroutines int
|
numberOfWorkerGoroutines int
|
||||||
// taskResultChan is a receive only channel for task results.
|
// taskResultChan is a receive only channel for task results.
|
||||||
taskResultChan chan *Result
|
taskResultChan chan *task.Result
|
||||||
// taskChan is a channel for tasks
|
// taskChan is a channel for tasks
|
||||||
taskChan chan *Task
|
taskChan chan *task.Task
|
||||||
// quinChan is a channel for sending the quit command to worker goroutines.
|
// quinChan is a channel for sending the quit command to worker goroutines.
|
||||||
quinChan chan int
|
quinChan chan int
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add support for per task execution functions
|
// executionFuncMap is a map that holds all the possible values for ExecutionFunc.
|
||||||
|
// Right now it is hard-coded but in the future it may be extended dynamically.
|
||||||
|
var executionFuncMap = map[string]ExecutionFunc{
|
||||||
|
"web_scrape": functions.WebScrapeTask,
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterNewExecutionFunction registers a new execution function.
|
||||||
|
func RegisterNewExecutionFunction(name string, function ExecutionFunc) error {
|
||||||
|
for n := range executionFuncMap {
|
||||||
|
if n == name {
|
||||||
|
return errors.New("function already exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
executionFuncMap[name] = function
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// NewDefaultExecutor returns a new instance of DefaultExecutor.
|
// NewDefaultExecutor returns a new instance of DefaultExecutor.
|
||||||
func NewDefaultExecutor(fn ExecutionFn) *DefaultExecutor {
|
func NewDefaultExecutor() *DefaultExecutor {
|
||||||
ws := &DefaultExecutor{
|
ws := &DefaultExecutor{
|
||||||
TaskExecutionFn: fn,
|
|
||||||
workerGroup: &sync.WaitGroup{},
|
workerGroup: &sync.WaitGroup{},
|
||||||
taskResultChan: make(chan *Result, 50),
|
taskResultChan: make(chan *task.Result, 50),
|
||||||
taskChan: make(chan *Task, 50),
|
taskChan: make(chan *task.Task, 50),
|
||||||
numberOfWorkerGoroutines: 5,
|
numberOfWorkerGoroutines: 5,
|
||||||
}
|
}
|
||||||
ws.quinChan = make(chan int, ws.numberOfWorkerGoroutines)
|
ws.quinChan = make(chan int, ws.numberOfWorkerGoroutines)
|
||||||
|
@ -52,12 +67,12 @@ func NewDefaultExecutor(fn ExecutionFn) *DefaultExecutor {
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddTask adds a task to the DefaultExecutor queue.
|
// AddTask adds a task to the DefaultExecutor queue.
|
||||||
func (ws *DefaultExecutor) AddTask(task *Task) {
|
func (ws *DefaultExecutor) AddTask(task *task.Task) {
|
||||||
ws.taskChan <- task
|
ws.taskChan <- task
|
||||||
}
|
}
|
||||||
|
|
||||||
// executeTask executes the given task using TaskExecutionFn
|
// executeTask executes the given task using DefaultTaskExecutionFuncName
|
||||||
func (ws *DefaultExecutor) executeTask(task *Task) error {
|
func (ws *DefaultExecutor) executeTask(task *task.Task) error {
|
||||||
var taskErr error = nil
|
var taskErr error = nil
|
||||||
// Execute task and set panics as errors in taskResult.
|
// Execute task and set panics as errors in taskResult.
|
||||||
func() {
|
func() {
|
||||||
|
@ -66,7 +81,16 @@ func (ws *DefaultExecutor) executeTask(task *Task) error {
|
||||||
taskErr = errors.New(fmt.Sprintf("panic: %s", r))
|
taskErr = errors.New(fmt.Sprintf("panic: %s", r))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
err := ws.TaskExecutionFn(task)
|
|
||||||
|
taskExecutionFunc, ok := executionFuncMap[task.ExecutionFuncName]
|
||||||
|
if !ok {
|
||||||
|
message := fmt.Sprintf("invalid task execution function name: '%s'", task.ExecutionFuncName)
|
||||||
|
logging.SugaredLogger.Error(message)
|
||||||
|
taskErr = errors.New(message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := taskExecutionFunc(task)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
taskErr = err
|
taskErr = err
|
||||||
}
|
}
|
||||||
|
@ -80,9 +104,10 @@ func (ws *DefaultExecutor) workerGoroutine() {
|
||||||
defer ws.workerGroup.Done()
|
defer ws.workerGroup.Done()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case task := <-ws.taskChan:
|
case currentTask := <-ws.taskChan:
|
||||||
var taskResult = NewResult(task)
|
var taskResult = task.NewResult(currentTask)
|
||||||
taskResult.error = ws.executeTask(task)
|
err := ws.executeTask(currentTask)
|
||||||
|
taskResult.SetError(err)
|
||||||
|
|
||||||
// Forward TaskResult to channel.
|
// Forward TaskResult to channel.
|
||||||
ws.taskResultChan <- taskResult
|
ws.taskResultChan <- taskResult
|
||||||
|
@ -95,7 +120,7 @@ func (ws *DefaultExecutor) workerGoroutine() {
|
||||||
|
|
||||||
// Start starts the DefaultExecutor.
|
// Start starts the DefaultExecutor.
|
||||||
// Start returns a receive only channel with task Result.
|
// Start returns a receive only channel with task Result.
|
||||||
func (ws *DefaultExecutor) Start() <-chan *Result {
|
func (ws *DefaultExecutor) Start() <-chan *task.Result {
|
||||||
// Start worker goroutines.
|
// Start worker goroutines.
|
||||||
for i := 0; i < ws.numberOfWorkerGoroutines; i++ {
|
for i := 0; i < ws.numberOfWorkerGoroutines; i++ {
|
||||||
ws.workerGroup.Add(1)
|
ws.workerGroup.Add(1)
|
98
task/executor/executor_test.go
Normal file
98
task/executor/executor_test.go
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"hotalert/alert"
|
||||||
|
"hotalert/task"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func randomHex(n int) (string, error) {
|
||||||
|
bytes := make([]byte, n)
|
||||||
|
if _, err := rand.Read(bytes); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(bytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_DefaultExecutor(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
var taskCounter = 0
|
||||||
|
var taskTestFunc = func(task *task.Task) error {
|
||||||
|
// First task is successful, others return error.
|
||||||
|
if taskCounter > 0 {
|
||||||
|
return errors.New("test")
|
||||||
|
}
|
||||||
|
taskCounter += 1
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := RegisterNewExecutionFunction("task_test", taskTestFunc)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
defaultExecutor := NewDefaultExecutor()
|
||||||
|
taskResultsChan := defaultExecutor.Start()
|
||||||
|
|
||||||
|
// Test
|
||||||
|
var task1 = &task.Task{
|
||||||
|
ExecutionFuncName: "task_test",
|
||||||
|
Timeout: 0,
|
||||||
|
Alerter: alert.NewDummyAlerter(),
|
||||||
|
Callback: nil,
|
||||||
|
}
|
||||||
|
var task2 = &task.Task{
|
||||||
|
ExecutionFuncName: "task_test",
|
||||||
|
Timeout: 0,
|
||||||
|
Alerter: alert.NewDummyAlerter(),
|
||||||
|
Callback: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultExecutor.AddTask(task1)
|
||||||
|
defaultExecutor.AddTask(task2)
|
||||||
|
|
||||||
|
// Assert results
|
||||||
|
result1 := <-taskResultsChan
|
||||||
|
assert.Equal(t, task1, result1.InitialTask)
|
||||||
|
assert.Equal(t, nil, result1.Error())
|
||||||
|
|
||||||
|
result2 := <-taskResultsChan
|
||||||
|
assert.Equal(t, task2, result2.InitialTask)
|
||||||
|
assert.Equal(t, errors.New("test"), result2.Error())
|
||||||
|
|
||||||
|
// Clean-up
|
||||||
|
defaultExecutor.Shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_DefaultExecutor_InvalidTaskExecutionFuncName(t *testing.T) {
|
||||||
|
defaultExecutor := NewDefaultExecutor()
|
||||||
|
taskResultsChan := defaultExecutor.Start()
|
||||||
|
|
||||||
|
// Test
|
||||||
|
var task1 = &task.Task{
|
||||||
|
ExecutionFuncName: "vand_dacia_2006",
|
||||||
|
Timeout: 0,
|
||||||
|
Alerter: alert.NewDummyAlerter(),
|
||||||
|
Callback: nil,
|
||||||
|
}
|
||||||
|
defaultExecutor.AddTask(task1)
|
||||||
|
|
||||||
|
// Assert results
|
||||||
|
result1 := <-taskResultsChan
|
||||||
|
assert.Equal(t, task1, result1.InitialTask)
|
||||||
|
assert.Equal(t, errors.New("invalid task execution function name: 'vand_dacia_2006'"), result1.Error())
|
||||||
|
|
||||||
|
// Clean-up
|
||||||
|
defaultExecutor.Shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_RegisterNewExecutionFunction(t *testing.T) {
|
||||||
|
var taskTestFunc = func(t *task.Task) error { return nil }
|
||||||
|
randomName, _ := randomHex(5)
|
||||||
|
err := RegisterNewExecutionFunction(randomName, taskTestFunc)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = RegisterNewExecutionFunction(randomName, taskTestFunc)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
|
@ -1,50 +0,0 @@
|
||||||
package task
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"hotalert/alert"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_DefaultExecutor(t *testing.T) {
|
|
||||||
// Setup
|
|
||||||
var taskCounter = 0
|
|
||||||
defaultExecutor := NewDefaultExecutor(func(task *Task) error {
|
|
||||||
// First task is successful, others return error.
|
|
||||||
if taskCounter > 0 {
|
|
||||||
return errors.New("test")
|
|
||||||
}
|
|
||||||
taskCounter += 1
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
taskResultsChan := defaultExecutor.Start()
|
|
||||||
|
|
||||||
// Test
|
|
||||||
var task1 = &Task{
|
|
||||||
Timeout: 0,
|
|
||||||
Alerter: alert.NewDummyAlerter(),
|
|
||||||
Callback: nil,
|
|
||||||
}
|
|
||||||
var task2 = &Task{
|
|
||||||
Timeout: 0,
|
|
||||||
Alerter: alert.NewDummyAlerter(),
|
|
||||||
Callback: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultExecutor.AddTask(task1)
|
|
||||||
defaultExecutor.AddTask(task2)
|
|
||||||
|
|
||||||
// Assert results
|
|
||||||
assert.Equal(t, &Result{
|
|
||||||
InitialTask: task1,
|
|
||||||
error: nil,
|
|
||||||
}, <-taskResultsChan)
|
|
||||||
assert.Equal(t, &Result{
|
|
||||||
InitialTask: task2,
|
|
||||||
error: errors.New("test"),
|
|
||||||
}, <-taskResultsChan)
|
|
||||||
|
|
||||||
// Clean-up
|
|
||||||
defaultExecutor.Shutdown()
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
package target
|
package functions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
@ -11,8 +11,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ScrapeWebTask scraps the web page given the task.
|
// WebScrapeTask scraps the web page given the task.
|
||||||
func ScrapeWebTask(task *task.Task) error {
|
func WebScrapeTask(task *task.Task) error {
|
||||||
// Parse options
|
// Parse options
|
||||||
targetUrl, ok := task.Options["url"].(string)
|
targetUrl, ok := task.Options["url"].(string)
|
||||||
if !ok {
|
if !ok {
|
|
@ -1,4 +1,4 @@
|
||||||
package target
|
package functions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -38,7 +38,7 @@ func TestScrapeWebTask(t *testing.T) {
|
||||||
"TestOk",
|
"TestOk",
|
||||||
task.Task{
|
task.Task{
|
||||||
Options: task.Options{
|
Options: task.Options{
|
||||||
"keywords": []string{"keyword"},
|
"keywords": []any{"keyword"},
|
||||||
},
|
},
|
||||||
Timeout: 10 * time.Second,
|
Timeout: 10 * time.Second,
|
||||||
Alerter: alert.NewDummyAlerter(),
|
Alerter: alert.NewDummyAlerter(),
|
||||||
|
@ -112,7 +112,7 @@ func TestScrapeWebTask(t *testing.T) {
|
||||||
|
|
||||||
tv.Task.Options["url"] = testHttpServer.URL
|
tv.Task.Options["url"] = testHttpServer.URL
|
||||||
|
|
||||||
err := ScrapeWebTask(&tv.Task)
|
err := WebScrapeTask(&tv.Task)
|
||||||
if tv.ExpectedError {
|
if tv.ExpectedError {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
} else {
|
} else {
|
18
task/task.go
18
task/task.go
|
@ -14,6 +14,8 @@ type Options map[string]any
|
||||||
|
|
||||||
// Task represents the context of a task.
|
// Task represents the context of a task.
|
||||||
type Task struct {
|
type Task struct {
|
||||||
|
// ExecutionFuncName is the function name associated with this task.
|
||||||
|
ExecutionFuncName string
|
||||||
// Options are the option given to the task.
|
// Options are the option given to the task.
|
||||||
Options Options `mapstructure:"options"`
|
Options Options `mapstructure:"options"`
|
||||||
// Timeout is the timeout for the task.
|
// Timeout is the timeout for the task.
|
||||||
|
@ -25,15 +27,16 @@ type Task struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTask returns a new task instance.
|
// NewTask returns a new task instance.
|
||||||
func NewTask(options Options, alerter alert.Alerter) *Task {
|
func NewTask(executionFuncName string, options Options, alerter alert.Alerter) *Task {
|
||||||
if alerter == nil {
|
if alerter == nil {
|
||||||
panic(fmt.Sprintf("Alerter cannot be nil"))
|
panic(fmt.Sprintf("Alerter cannot be nil"))
|
||||||
}
|
}
|
||||||
return &Task{
|
return &Task{
|
||||||
Options: options,
|
ExecutionFuncName: executionFuncName,
|
||||||
Timeout: 10 * time.Second,
|
Options: options,
|
||||||
Alerter: alerter,
|
Timeout: 10 * time.Second,
|
||||||
Callback: nil,
|
Alerter: alerter,
|
||||||
|
Callback: nil,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +56,11 @@ func NewResult(task *Task) *Result {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetError sets the error on the result object.
|
||||||
|
func (r *Result) SetError(err error) {
|
||||||
|
r.error = err
|
||||||
|
}
|
||||||
|
|
||||||
// Error returns the error encountered during the execution of the task.
|
// Error returns the error encountered during the execution of the task.
|
||||||
// Error returns null if the task had no errors and was completed.
|
// Error returns null if the task had no errors and was completed.
|
||||||
func (r *Result) Error() error {
|
func (r *Result) Error() error {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package task
|
package task
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"hotalert/alert"
|
"hotalert/alert"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -8,10 +9,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_NewTask(t *testing.T) {
|
func Test_NewTask(t *testing.T) {
|
||||||
var task = NewTask(Options{
|
var task = NewTask("web_scrape", Options{
|
||||||
"option": "true",
|
"option": "true",
|
||||||
}, alert.NewDummyAlerter())
|
}, alert.NewDummyAlerter())
|
||||||
assert.Equal(t, Task{
|
assert.Equal(t, Task{
|
||||||
|
ExecutionFuncName: "web_scrape",
|
||||||
Options: Options{
|
Options: Options{
|
||||||
"option": "true",
|
"option": "true",
|
||||||
},
|
},
|
||||||
|
@ -22,12 +24,15 @@ func Test_NewTask(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_NewResult(t *testing.T) {
|
func Test_NewResult(t *testing.T) {
|
||||||
var task = NewTask(Options{
|
var task = NewTask("web_scrape", Options{
|
||||||
"option": "true",
|
"option": "true",
|
||||||
}, alert.NewDummyAlerter())
|
}, alert.NewDummyAlerter())
|
||||||
|
testError := errors.New("test error")
|
||||||
var result = NewResult(task)
|
var result = NewResult(task)
|
||||||
|
result.SetError(testError)
|
||||||
assert.Equal(t, Result{
|
assert.Equal(t, Result{
|
||||||
InitialTask: &Task{
|
InitialTask: &Task{
|
||||||
|
ExecutionFuncName: "web_scrape",
|
||||||
Options: Options{
|
Options: Options{
|
||||||
"option": "true",
|
"option": "true",
|
||||||
},
|
},
|
||||||
|
@ -35,6 +40,6 @@ func Test_NewResult(t *testing.T) {
|
||||||
Alerter: alert.NewDummyAlerter(),
|
Alerter: alert.NewDummyAlerter(),
|
||||||
Callback: nil,
|
Callback: nil,
|
||||||
},
|
},
|
||||||
error: nil,
|
error: testError,
|
||||||
}, *result)
|
}, *result)
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,8 +108,14 @@ func (p *Workload) buildTasksArray(workloadData map[string]any) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
executionFuncName, ok := taskEntry["function"].(string)
|
||||||
|
if !ok {
|
||||||
|
logging.SugaredLogger.Errorf("error parsing entry %d in tasks array: invalid function name", i)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Build task
|
// Build task
|
||||||
tempTask := task.NewTask(taskOptions, alert.DummyAlerter{})
|
tempTask := task.NewTask(executionFuncName, taskOptions, alert.DummyAlerter{})
|
||||||
|
|
||||||
// Timeout (optional)
|
// Timeout (optional)
|
||||||
taskTimeout, ok := taskEntry["timeout"].(int)
|
taskTimeout, ok := taskEntry["timeout"].(int)
|
||||||
|
|
|
@ -15,6 +15,7 @@ tasks:
|
||||||
keywords: ["Software Engineer, Backend"]
|
keywords: ["Software Engineer, Backend"]
|
||||||
timeout: 10
|
timeout: 10
|
||||||
alerter: "webhook_discord"
|
alerter: "webhook_discord"
|
||||||
|
function: "web_scrape"
|
||||||
- options:
|
- options:
|
||||||
url: https://jobs.ro
|
url: https://jobs.ro
|
||||||
keywords: ["Software Engineer, Front-End", "Software Architect"]
|
keywords: ["Software Engineer, Front-End", "Software Architect"]
|
||||||
|
@ -23,6 +24,7 @@ tasks:
|
||||||
extra_bool: True
|
extra_bool: True
|
||||||
timeout: 15
|
timeout: 15
|
||||||
alerter: "webhook_discord"
|
alerter: "webhook_discord"
|
||||||
|
function: "web_scrape"
|
||||||
alerts:
|
alerts:
|
||||||
webhook_discord:
|
webhook_discord:
|
||||||
webhook: https://webhook.url.com
|
webhook: https://webhook.url.com
|
||||||
|
@ -69,6 +71,7 @@ tasks:
|
||||||
keywords: ["Software Engineer, Backend"]
|
keywords: ["Software Engineer, Backend"]
|
||||||
timeout: 10
|
timeout: 10
|
||||||
alerter: "webhook_discord"
|
alerter: "webhook_discord"
|
||||||
|
function: "web_scrape"
|
||||||
- options:
|
- options:
|
||||||
url: https://jobs.ro
|
url: https://jobs.ro
|
||||||
keywords: ["Software Engineer, Front-End", "Software Architect"]
|
keywords: ["Software Engineer, Front-End", "Software Architect"]
|
||||||
|
@ -77,6 +80,7 @@ tasks:
|
||||||
extra_bool: True
|
extra_bool: True
|
||||||
timeout: 15
|
timeout: 15
|
||||||
alerter: "webhook_discord"
|
alerter: "webhook_discord"
|
||||||
|
function: "web_scrape"
|
||||||
alerts:
|
alerts:
|
||||||
webhook_discord:
|
webhook_discord:
|
||||||
webhook: https://webhook.url.com
|
webhook: https://webhook.url.com
|
||||||
|
@ -100,6 +104,7 @@ tasks:
|
||||||
keywords: ["Software Engineer, Backend"]
|
keywords: ["Software Engineer, Backend"]
|
||||||
timeout: 10
|
timeout: 10
|
||||||
alerter: "webhook_discord"
|
alerter: "webhook_discord"
|
||||||
|
function: "web_scrape"
|
||||||
- options:
|
- options:
|
||||||
url: https://jobs.ro
|
url: https://jobs.ro
|
||||||
keywords: ["Software Engineer, Front-End", "Software Architect"]
|
keywords: ["Software Engineer, Front-End", "Software Architect"]
|
||||||
|
@ -108,6 +113,7 @@ tasks:
|
||||||
extra_bool: True
|
extra_bool: True
|
||||||
timeout: 15
|
timeout: 15
|
||||||
alerter: "webhook_discord"
|
alerter: "webhook_discord"
|
||||||
|
function: "web_scrape"
|
||||||
alerts:
|
alerts:
|
||||||
webhook_discord:
|
webhook_discord:
|
||||||
webhook: https://webhook.url.com
|
webhook: https://webhook.url.com
|
||||||
|
@ -127,6 +133,7 @@ tasks:
|
||||||
keywords: ["Software Engineer, Backend"]
|
keywords: ["Software Engineer, Backend"]
|
||||||
timeout: 10
|
timeout: 10
|
||||||
alerter: "webhook_discord"
|
alerter: "webhook_discord"
|
||||||
|
function: "web_scrape"
|
||||||
- options:
|
- options:
|
||||||
url: https://jobs.ro
|
url: https://jobs.ro
|
||||||
keywords: ["Software Engineer, Front-End", "Software Architect"]
|
keywords: ["Software Engineer, Front-End", "Software Architect"]
|
||||||
|
@ -135,6 +142,7 @@ tasks:
|
||||||
extra_bool: True
|
extra_bool: True
|
||||||
timeout: 15
|
timeout: 15
|
||||||
alerter: "webhook_discord"
|
alerter: "webhook_discord"
|
||||||
|
function: "web_scrape"
|
||||||
alertx:
|
alertx:
|
||||||
webhook_discord:
|
webhook_discord:
|
||||||
webhook: https://webhook.url.com
|
webhook: https://webhook.url.com
|
||||||
|
@ -149,6 +157,7 @@ tasks:
|
||||||
keywords: ["Software Engineer, Backend"]
|
keywords: ["Software Engineer, Backend"]
|
||||||
timeout: 10
|
timeout: 10
|
||||||
alerter: "webhook_discord"
|
alerter: "webhook_discord"
|
||||||
|
function: "web_scrape"
|
||||||
- options:
|
- options:
|
||||||
url: https://jobs.ro
|
url: https://jobs.ro
|
||||||
keywords: ["Software Engineer, Front-End", "Software Architect"]
|
keywords: ["Software Engineer, Front-End", "Software Architect"]
|
||||||
|
@ -157,6 +166,7 @@ tasks:
|
||||||
extra_bool: True
|
extra_bool: True
|
||||||
timeout: 15
|
timeout: 15
|
||||||
alerter: "webhook_discord"
|
alerter: "webhook_discord"
|
||||||
|
function: "web_scrape"
|
||||||
alerts: "I'm just a string please don't hurt me."
|
alerts: "I'm just a string please don't hurt me."
|
||||||
`
|
`
|
||||||
|
|
||||||
|
@ -167,6 +177,7 @@ tasks:
|
||||||
keywords: ["Software Engineer, Backend"]
|
keywords: ["Software Engineer, Backend"]
|
||||||
timeout: 10
|
timeout: 10
|
||||||
alerter: "webhook_discord"
|
alerter: "webhook_discord"
|
||||||
|
function: "web_scrape"
|
||||||
- options:
|
- options:
|
||||||
url: https://jobs.ro
|
url: https://jobs.ro
|
||||||
keywords: ["Software Engineer, Front-End", "Software Architect"]
|
keywords: ["Software Engineer, Front-End", "Software Architect"]
|
||||||
|
@ -175,6 +186,7 @@ tasks:
|
||||||
extra_bool: True
|
extra_bool: True
|
||||||
timeout: 15
|
timeout: 15
|
||||||
alerter: "webhook_discord"
|
alerter: "webhook_discord"
|
||||||
|
function: "web_scrape"
|
||||||
alerts:
|
alerts:
|
||||||
webhook_discord:
|
webhook_discord:
|
||||||
webhook: https://webhook.url.com
|
webhook: https://webhook.url.com
|
||||||
|
@ -194,6 +206,7 @@ tasks:
|
||||||
keywords: ["Software Engineer, Backend"]
|
keywords: ["Software Engineer, Backend"]
|
||||||
timeout: 10
|
timeout: 10
|
||||||
alerter: "webhook_discord"
|
alerter: "webhook_discord"
|
||||||
|
function: "web_scrape"
|
||||||
- options:
|
- options:
|
||||||
url: https://jobs.ro
|
url: https://jobs.ro
|
||||||
keywords: ["Software Engineer, Front-End", "Software Architect"]
|
keywords: ["Software Engineer, Front-End", "Software Architect"]
|
||||||
|
@ -202,6 +215,7 @@ tasks:
|
||||||
extra_bool: True
|
extra_bool: True
|
||||||
timeout: 15
|
timeout: 15
|
||||||
alerter: "webhook_discord"
|
alerter: "webhook_discord"
|
||||||
|
function: "web_scrape"
|
||||||
alerts:
|
alerts:
|
||||||
imcoolalerter:
|
imcoolalerter:
|
||||||
cool: true
|
cool: true
|
||||||
|
@ -241,6 +255,7 @@ tasks:
|
||||||
keywords: ["Software Engineer, Backend"]
|
keywords: ["Software Engineer, Backend"]
|
||||||
timeout: 10
|
timeout: 10
|
||||||
alerter: "imaacoolalerter"
|
alerter: "imaacoolalerter"
|
||||||
|
function: "web_scrape"
|
||||||
alerts:
|
alerts:
|
||||||
webhook_discord:
|
webhook_discord:
|
||||||
webhook: https://webhook.url.com
|
webhook: https://webhook.url.com
|
||||||
|
@ -316,11 +331,13 @@ tasks:
|
||||||
keywords: ["Software Engineer, Backend"]
|
keywords: ["Software Engineer, Backend"]
|
||||||
timeout: 10
|
timeout: 10
|
||||||
alerter: "imaacoolalerter"
|
alerter: "imaacoolalerter"
|
||||||
|
function: "web_scrape"
|
||||||
- options:
|
- options:
|
||||||
url: https://jobs.eu
|
url: https://jobs.eu
|
||||||
keywords: ["Software Engineer, Backend"]
|
keywords: ["Software Engineer, Backend"]
|
||||||
timeout: 10
|
timeout: 10
|
||||||
alerter: "webhook_discord"
|
alerter: "webhook_discord"
|
||||||
|
function: "web_scrape"
|
||||||
alerts:
|
alerts:
|
||||||
webhook_discord:
|
webhook_discord:
|
||||||
webhook: https://webhook.url.com
|
webhook: https://webhook.url.com
|
||||||
|
@ -334,3 +351,30 @@ func Test_FromYamlContent_InvalidAlerterForTask(t *testing.T) {
|
||||||
assert.NotNil(t, currentWorkload)
|
assert.NotNil(t, currentWorkload)
|
||||||
assert.Len(t, currentWorkload.tasksList, 1)
|
assert.Len(t, currentWorkload.tasksList, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var testTasksTaskHasInvalidExecutionFunction = `
|
||||||
|
tasks:
|
||||||
|
- options:
|
||||||
|
url: https://jobs.eu
|
||||||
|
keywords: ["Software Engineer, Backend"]
|
||||||
|
timeout: 10
|
||||||
|
alerter: "imaacoolalerter"
|
||||||
|
- options:
|
||||||
|
url: https://jobs.eu
|
||||||
|
keywords: ["Software Engineer, Backend"]
|
||||||
|
timeout: 10
|
||||||
|
alerter: "webhook_discord"
|
||||||
|
function: "web_scrape"
|
||||||
|
alerts:
|
||||||
|
webhook_discord:
|
||||||
|
webhook: https://webhook.url.com
|
||||||
|
# $keywords can be used as a placeholder in the message, and it will be replaced with the actual keywords.
|
||||||
|
message: "Hi, the keyword $keywords was found on page!"
|
||||||
|
`
|
||||||
|
|
||||||
|
func Test_FromYamlContent_InvalidExecutionFuncNameForTask(t *testing.T) {
|
||||||
|
currentWorkload, err := FromYamlContent([]byte(testTasksTaskHasInvalidExecutionFunction))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, currentWorkload)
|
||||||
|
assert.Len(t, currentWorkload.tasksList, 1)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue