Skip to content

Commit

Permalink
Merge pull request #64 from balena-io-modules/run-task
Browse files Browse the repository at this point in the history
Add test utility to run a task
  • Loading branch information
flowzone-app[bot] authored Apr 10, 2024
2 parents a24196c + dec3668 commit 2317d4c
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 19 deletions.
25 changes: 8 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,25 +405,16 @@ Objects generated by task constructors are callable, and receive part of the `Co

```typescript
// Another test utility
import { zip } from 'mahler/testing';
import { runTask } from 'mahler/testing';

// plusOneAction is of type `Action`
const plusOneAction = plusOne({ target: 3 });
// This executes the task. Useful for testing
// `runTask` calls the task with the given state and context
// the call will throw if the task condition fails
console.log(await runTask(plusOne, 0, { target: 3 })); // 1

// `zip` packs the action (including condition) and returns
// a callable function
const doPlusOne = zip(plusOneAction);

// This executes the action. Useful for testing
console.log(await doPlusOne(0)); // 1

// plusTwoMethod is of type `Method`
const plusTwoMethod = plusTwo({ target: 3 });

// zip in this case expands the method into its actions so it can be applied
// as if it was a function
const doPlusTwo = zip(plusTwoMethod);
console.log(await doPlusTwo(0)); // 2
// `runTask` in this case expands the method into its actions and
// applies the actions sequentially
console.log(await runTask(doPlusTwo, 0, { target: 3 })); // 2
```

Methods are useful for tweaking the plans under certain conditions. They also help reduce the search space. When looking for a plan, the Planner will try methods first, and only if methods fail, proceed to look for action tasks. During planning, the method is expanded recursively into its component actions, so they won't appear on the final plan.
Expand Down
1 change: 1 addition & 0 deletions lib/testing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './mermaid';
export * from './builder';
export * from './stringify';
export * from './zip';
export * from './run-task';
57 changes: 57 additions & 0 deletions lib/testing/run-task.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { expect } from '~/test-utils';
import { Task } from '../task';
import { runTask } from './run-task';

describe('testing/run-task', () => {
const plusOne = Task.of<number>().from({
// This means the task can only be triggered
// if the system state is below the target
condition: (state, { target }) => state < target,
// The effect of the action is increasing the system
// counter by 1
effect: (state) => ++state._,
// An optional description. Useful for testing
description: '+1',
});

const plusTwo = Task.of<number>().from({
condition: (state, { target }) => target - state > 1,
method: (_, { target }) => [plusOne({ target }), plusOne({ target })],
description: '+2',
});

const plusThree = Task.of<number>().from({
condition: (state, { target }) => target - state > 2,
method: (_, { target }) => [plusTwo({ target }), plusOne({ target })],
description: '+3',
});

const buggedPlusThree = Task.of<number>().from({
method: (_, { target }) => [plusTwo({ target }), plusOne({ target })],
description: '+3',
});

it('runs an action task if the condition is met', async () => {
expect(await runTask(plusOne, 1, { target: 2 })).to.equal(2);
expect(await runTask(plusOne, 0, { target: 2 })).to.equal(1);
});

it('throws if the condition of an action task is not met', async () => {
await expect(runTask(plusOne, 2, { target: 2 })).to.be.rejected;
await expect(runTask(plusOne, 3, { target: 2 })).to.be.rejected;
});

it('runs a method task by expanding its actions', async () => {
expect(await runTask(plusTwo, 0, { target: 2 })).to.equal(2);
expect(await runTask(plusTwo, 1, { target: 4 })).to.equal(3);
expect(await runTask(plusThree, 1, { target: 4 })).to.equal(4);
});

it('throws if a condition of a method task is not met', async () => {
await expect(runTask(plusTwo, 2, { target: 2 })).to.be.rejected;
await expect(runTask(plusTwo, 3, { target: 4 })).to.be.rejected;
await expect(runTask(plusThree, 2, { target: 4 })).to.be.rejected;
// the condition for the plusTwo call should fail here
await expect(runTask(buggedPlusThree, 3, { target: 4 })).to.be.rejected;
});
});
23 changes: 23 additions & 0 deletions lib/testing/run-task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { zip } from './zip';

import type { Task, TaskOp, TaskArgs } from '../task';
import type { Root, PathType } from '../path';

/**
* Run the task on a given state and context
*
* If the given task is a Method task, it expands the task first in
* a sequential fashion and runs all the returned actions
*/
export async function runTask<
TState = unknown,
TPath extends PathType = Root,
TOp extends TaskOp = 'update',
>(
task: Task<TState, TPath, TOp>,
state: TState,
args: TaskArgs<TState, TPath, TOp>,
): Promise<TState> {
const doTask = zip(task(args));
return doTask(state);
}
8 changes: 6 additions & 2 deletions lib/testing/zip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { Method } from '../task';

function expand<T>(s: T, method: Method<T>): Array<Action<T, any, any>> {
if (!method.condition(s)) {
return [];
throw new Error(
`${method.description}: condition not met for expanding method`,
);
}

const res = method(s);
Expand Down Expand Up @@ -35,7 +37,9 @@ export function zip<T>(

for (const action of actions) {
if (!action.condition(s)) {
return s;
throw new Error(
`${action.description}: condition not met for running action`,
);
}
await action(ref);
}
Expand Down

0 comments on commit 2317d4c

Please sign in to comment.