Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add test task for ligo plugin #1255

Merged
merged 6 commits into from
Sep 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions taqueria-plugin-ligo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,73 @@ Lastly, `taq compile hello.mligo` will compile `hello.mligo` and emit `hello.tz`

None for now.

## The `taq test` Task

Basic usage is:

```shell
taq test <filename>
```

### Basic description
This task tests the LIGO source code and ouputs a result suggesting a failure or success. Normally you'd have a contract file and a separate test file that includes the contract's code, both within the `/contracts` directory.

For example, refer to the following 2 code snippets:
```ligo title="counter.mligo"
type storage = int

type parameter =
Increment of int
| Decrement of int
| Reset

type return = operation list * storage

// Two entrypoints

let add (store, delta : storage * int) : storage = store + delta
let sub (store, delta : storage * int) : storage = store - delta

(* Main access point that dispatches to the entrypoints according to
the smart contract parameter. *)

let main (action, store : parameter * storage) : return =
([] : operation list), // No operations
(match action with
Increment (n) -> add (store, n)
| Decrement (n) -> sub (store, n)
| Reset -> 0)
```

```ligo title="testCounter.mligo"
#include "counter.mligo"

let initial_storage = 42

let test_initial_storage =
let (taddr, _, _) = Test.originate main initial_storage 0tez in
assert (Test.get_storage taddr = initial_storage)

let test_increment =
let (taddr, _, _) = Test.originate main initial_storage 0tez in
let contr = Test.to_contract taddr in
let _ = Test.transfer_to_contract_exn contr (Increment 1) 1mutez in
assert (Test.get_storage taddr = initial_storage + 1)
```

By running `taq test testCounter.mligo`, you should get the following:
```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Contract β”‚ Test Results β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ testCounter.mligo β”‚ Everything at the top-level was executed. β”‚
β”‚ β”‚ - test_initial_storage exited with value (). β”‚
β”‚ β”‚ - test_increment exited with value (). β”‚
β”‚ β”‚ β”‚
β”‚ β”‚ πŸŽ‰ All tests passed πŸŽ‰ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

## Template creation
The LIGO plugin also exposes a contract template via the `taq create contract <contractName>` task. This task will create a new LIGO contract in the `contracts` directory, insert some boilerplate LIGO contract code and will register the contract with Taqueria

Expand Down
67 changes: 67 additions & 0 deletions taqueria-plugin-ligo/_readme.eta
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,73 @@ Make sure you name it `hello.storages.mligo` and not `hello.storage.mligo` (note

None for now.

## The `taq test` Task

Basic usage is:

```shell
taq test <filename>
```

### Basic description
This task tests the LIGO source code and ouputs a result suggesting a failure or success. Normally you'd have a contract file and a separate test file that includes the contract's code, both within the `/contracts` directory.

For example, refer to the following 2 code snippets:
```ligo title="counter.mligo"
type storage = int

type parameter =
Increment of int
| Decrement of int
| Reset

type return = operation list * storage

// Two entrypoints

let add (store, delta : storage * int) : storage = store + delta
let sub (store, delta : storage * int) : storage = store - delta

(* Main access point that dispatches to the entrypoints according to
the smart contract parameter. *)

let main (action, store : parameter * storage) : return =
([] : operation list), // No operations
(match action with
Increment (n) -> add (store, n)
| Decrement (n) -> sub (store, n)
| Reset -> 0)
```

```ligo title="testCounter.mligo"
#include "counter.mligo"

let initial_storage = 42

let test_initial_storage =
let (taddr, _, _) = Test.originate main initial_storage 0tez in
assert (Test.get_storage taddr = initial_storage)

let test_increment =
let (taddr, _, _) = Test.originate main initial_storage 0tez in
let contr = Test.to_contract taddr in
let _ = Test.transfer_to_contract_exn contr (Increment 1) 1mutez in
assert (Test.get_storage taddr = initial_storage + 1)
```

By running `taq test testCounter.mligo`, you should get the following:
```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Contract β”‚ Test Results β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ testCounter.mligo β”‚ Everything at the top-level was executed. β”‚
β”‚ β”‚ - test_initial_storage exited with value (). β”‚
β”‚ β”‚ - test_increment exited with value (). β”‚
β”‚ β”‚ β”‚
β”‚ β”‚ πŸŽ‰ All tests passed πŸŽ‰ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

## Template creation
The LIGO plugin also exposes a contract template via the `taq create contract <contractName>` task. This task will create a new LIGO contract in the `contracts` directory, insert some boilerplate LIGO contract code and will register the contract with Taqueria

Expand Down
3 changes: 1 addition & 2 deletions taqueria-plugin-ligo/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ const compileExprs = (parsedArgs: Opts, sourceFile: string, exprKind: ExprKind):
})
.then(mergeArtifactsOutput(sourceFile));

const compileContractWithStorageAndParameter = async (parsedArgs: Opts, sourceFile: string) => {
const compileContractWithStorageAndParameter = async (parsedArgs: Opts, sourceFile: string): Promise<TableRow[]> => {
const contractCompileResult = await compileContract(parsedArgs, sourceFile);
if (contractCompileResult.artifact === COMPILE_ERR_MSG) return [contractCompileResult];

Expand Down Expand Up @@ -226,7 +226,6 @@ const mergeArtifactsOutput = (sourceFile: string) =>

export const compile = (parsedArgs: Opts): Promise<void> => {
const sourceFile = parsedArgs.sourceFile;
if (!sourceFile) return sendAsyncErr('No source file specified.');
let p: Promise<TableRow[]>;
if (isStoragesFile(sourceFile)) p = compileExprs(parsedArgs, sourceFile, 'storage');
else if (isParametersFile(sourceFile)) p = compileExprs(parsedArgs, sourceFile, 'parameter');
Expand Down
7 changes: 7 additions & 0 deletions taqueria-plugin-ligo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ Plugin.create(i18n => ({
handler: 'proxy',
encoding: 'json',
}),
Task.create({
task: 'test',
command: 'test <sourceFile>',
description: 'Test a smart contract written in LIGO',
handler: 'proxy',
encoding: 'json',
}),
],
templates: [
Template.create({
Expand Down
5 changes: 3 additions & 2 deletions taqueria-plugin-ligo/ligo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { sendAsyncErr } from '@taqueria/node-sdk';
import { RequestArgs } from '@taqueria/node-sdk/types';
import compile from './compile';
import { test } from './test';

interface Opts extends RequestArgs.ProxyRequestArgs {
sourceFile: string;
Expand All @@ -10,8 +11,8 @@ export const ligo = (parsedArgs: Opts): Promise<void> => {
switch (parsedArgs.task) {
case 'compile':
return compile(parsedArgs);
// case 'test':
// return test(parsedArgs); // TODO: to be implemented in the future
case 'test':
return test(parsedArgs);
default:
return sendAsyncErr(`${parsedArgs.task} is not an understood task by the LIGO plugin`);
}
Expand Down
52 changes: 52 additions & 0 deletions taqueria-plugin-ligo/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { execCmd, getArch, sendAsyncErr, sendErr, sendJsonRes, sendWarn } from '@taqueria/node-sdk';
import { RequestArgs } from '@taqueria/node-sdk/types';
import { join } from 'path';

interface Opts extends RequestArgs.t {
sourceFile: string;
}

type TableRow = { contract: string; testResults: string };

const getInputFilename = (parsedArgs: Opts, sourceFile: string): string =>
join(parsedArgs.config.contractsDir, sourceFile);

const getTestContractCmd = (parsedArgs: Opts, sourceFile: string): string => {
const projectDir = process.env.PROJECT_DIR ?? parsedArgs.projectDir;
if (!projectDir) throw `No project directory provided`;
const baseCmd =
`DOCKER_DEFAULT_PLATFORM=linux/amd64 docker run --rm -v \"${projectDir}\":/project -w /project -u $(id -u):$(id -g) ligolang/ligo:next run test`;
const inputFile = getInputFilename(parsedArgs, sourceFile);
const cmd = `${baseCmd} ${inputFile}`;
return cmd;
};

const testContract = (parsedArgs: Opts, sourceFile: string): Promise<TableRow> =>
getArch()
.then(() => getTestContractCmd(parsedArgs, sourceFile))
.then(execCmd)
.then(({ stdout, stderr }) => {
if (stderr.length > 0) sendWarn(stderr);
const result = 'πŸŽ‰ All tests passed πŸŽ‰';
return {
contract: sourceFile,
testResults: stdout.length > 0 ? `${stdout}\n${result}` : result,
};
})
.catch(err => {
sendErr(`\n=== For ${sourceFile} ===`);
sendErr(err.message.replace(/Command failed.+?\n/, ''));
return {
contract: sourceFile,
testResults: 'Some tests failed :(',
};
});

export const test = (parsedArgs: Opts): Promise<void> => {
const sourceFile = parsedArgs.sourceFile;
return testContract(parsedArgs, sourceFile).then(result => [result]).then(sendJsonRes).catch(err =>
sendAsyncErr(err, false)
);
};

export default test;
13 changes: 13 additions & 0 deletions tests/e2e/data/hello-tacos-invalid-tests.mligo
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#include "hello-tacos.mligo"

let available_tacos = 100

let test_available_tacos =
let (taddr, _, _) = Test.originate main initial_storage 100tez in
assert (Test.get_storage taddr = available_tacos)

let test_buy_tacos =
let (taddr, _, _) = Test.originate main initial_storage 100tez in
let contr = Test.to_contract taddr in
let _ = Test.transfer_to_contract_exn contr 1mutez in
assert (Test.get_storage taddr = test_available_tacos - 1)
12 changes: 12 additions & 0 deletions tests/e2e/data/hello-tacos-tests.mligo
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#include "hello-tacos.mligo"

let available_tacos = 100n

let test_available_tacos =
let (taddr, _, _) = Test.originate main available_tacos 100tez in
assert (Test.get_storage taddr = available_tacos)

let test_buy_tacos =
let res = main(1n, available_tacos) in (* The nature of the contract we don't have entrypoints
// The only way to test it is to call function itself *)
assert (res.1 = 99n)
2 changes: 2 additions & 0 deletions tests/e2e/data/help-contents/help-contents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Commands:
taq list-contracts List registered contracts
taq compile Provided by more than one plugin. The option -
-plugin is required.
taq test <sourceFile> Test a smart contract written in LIGO
taq create <template> Create files from pre-existing templates

Options:
Expand Down Expand Up @@ -96,6 +97,7 @@ Commands:
taq list-contracts List registered contracts
taq compile Provided by more than one plugin. The option -
-plugin is required.
taq test <sourceFile> Test a smart contract written in LIGO
taq create <template> Create files from pre-existing templates

Options:
Expand Down
1 change: 1 addition & 0 deletions tests/e2e/data/help-contents/ligo-contents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Commands:
tax to Michelson code, along with its associat
ed storages and parameters files if they are f
ound [aliases: c, compile-ligo]
taq test <sourceFile> Test a smart contract written in LIGO
taq create <template> Create files from pre-existing templates

Options:
Expand Down
40 changes: 40 additions & 0 deletions tests/e2e/taqueria-plugin-ligo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,46 @@ describe('E2E Testing for taqueria ligo plugin', () => {
}
});

test('Verify that taqueria ligo plugin can run ligo test using taq test <sourceFile> command', async () => {
try {
// 1. Copy contract and tests files from data folder to taqueria project folder
await exec(`cp e2e/data/hello-tacos-tests.mligo ${taqueriaProjectPath}/contracts`);

// 2. Run taq test ${testFileName}
const { stdout, stderr } = await exec(`taq test hello-tacos-tests.mligo`, { cwd: `./${taqueriaProjectPath}` });
expect(stdout).toContain('All tests passed');
} catch (error) {
throw new Error(`error: ${error}`);
}
});

test('Verify that taqueria ligo plugin will output proper error message running taq test <sourceFile> command against invalid test file', async () => {
try {
// 1. Copy contract and tests files from data folder to taqueria project folder
await exec(`cp e2e/data/hello-tacos-invalid-tests.mligo ${taqueriaProjectPath}/contracts`);

// 2. Run taq test ${testFileName}
// const output = await exec(`taq test hello-tacos-invalid-tests.mligo`, { cwd: `./${taqueriaProjectPath}` });
const { stdout, stderr } = await exec(`taq test hello-tacos-invalid-tests.mligo`, {
cwd: `./${taqueriaProjectPath}`,
});
expect(stdout).toContain('Some tests failed :(');
expect(stderr).toContain('Variable "initial_storage" not found.');
} catch (error) {
throw new Error(`error: ${error}`);
}
});

test('Verify that taqueria ligo plugin will output proper error message running taq test <sourceFile> command against non-existing file', async () => {
try {
// 1. Run taq test ${testFileName} against file that does not exist
const { stdout, stderr } = await exec(`taq test hello-tacos-test.mligo`, { cwd: `./${taqueriaProjectPath}` });
expect(stderr).toContain('contracts/hello-tacos-test.mligo: No such file or directory');
} catch (error) {
throw new Error(`error: ${error}`);
}
});

test.skip('Verify that the LIGO contract template is instantiated with the right content and registered', async () => {
try {
await exec(`taq create contract counter.mligo`, { cwd: `./${taqueriaProjectPath}` });
Expand Down
Loading