Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: aragon/taiko-contracts
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 91cb44c0e2a19ad0ddf2fff9ea2737746b195c9c
Choose a base ref
..
head repository: aragon/taiko-contracts
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: b5353ae5aee49012e8084af5812216b44484bf0b
Choose a head ref
Showing with 281 additions and 22 deletions.
  1. +2 −0 .gitignore
  2. +50 −22 Makefile
  3. +75 −0 README.md
  4. +3 −0 TEST_TREE.md
  5. +151 −0 test/script/make-test-tree.ts
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -13,3 +13,5 @@ docs/

lcov.info
.DS_Store

*.tree
72 changes: 50 additions & 22 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,43 +1,71 @@
.DEFAULT_TARGET: help

SOLIDITY_VERSION=0.8.17
TREE_FILES=$(wildcard test/*.tree test/integration/*.tree)
MOUNTED_PATH=/data
SOURCE_FILES=$(wildcard test/*.t.yaml test/integration/*.t.yaml)
TREE_FILES = $(SOURCE_FILES:.t.yaml=.tree)
TARGET_TEST_FILES = $(SOURCE_FILES:.tree=.t.sol)
MAKE_TEST_TREE=deno run ./test/script/make-test-tree.ts
TEST_TREE_MARKDOWN=TEST_TREE.md

.PHONY: help
help:
@echo "Available targets:"
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
| sed -n 's/^\(.*\): \(.*\)##\(.*\)/- \1:\t\3/p'
| sed -n 's/^\(.*\): \(.*\)##\(.*\)/- make \1 \3/p'

# SYNC TEST FILES
all: sync markdown ## Builds all tree files and updates the test tree markdown

.PHONY: sync
sync: ## Scaffold or sync tree files into tests
@docker run --rm -it -v .:/data nixos/nix nix-shell -p bulloak gnumake \
--command "cd $(MOUNTED_PATH) && make sync-tree"

.PHONY: sync-tree
sync-tree: $(TREE_FILES)
@echo "Syncing tree files"
sync: $(TREE_FILES) ## Scaffold or sync tree files into solidity tests
@for file in $^; do \
if [ ! -f $${file%.tree}.t.sol ]; then \
echo "[Scaffold] $${file%.tree}.t.sol" ; \
bulloak scaffold -s $(SOLIDITY_VERSION) -w $$file ; \
bulloak scaffold -s $(SOLIDITY_VERSION) --vm-skip -w $$file ; \
else \
echo "[Sync file] $${file%.tree}.t.sol" ; \
bulloak check --fix $$file ; \
fi \
done

# CHECK TEST FILES
check: $(TREE_FILES) ## Checks if solidity files are out of sync
bulloak check $^

markdown: $(TEST_TREE_MARKDOWN) ## Generates a markdown file with the test definitions rendered as a tree

.PHONY: check
check: ## Scaffold or sync tree files into tests
@docker run --rm -it -v .:/data nixos/nix nix-shell -p bulloak gnumake \
--command "cd $(MOUNTED_PATH) && make check-tree"
# Internal targets

.PHONY: check-tree
check-tree: $(TREE_FILES)
@echo "Checking tree files"
bulloak check $^
# Generate a markdown file with the test trees
$(TEST_TREE_MARKDOWN): $(TREE_FILES)
@echo "[Markdown] TEST_TREE.md"
@echo "# Test tree definitions" > $@
@echo "" >> $@
@echo "Below is the graphical definition of the contract tests implemented on [the test folder](./test)" >> $@
@echo "" >> $@

@for file in $^; do \
echo "\`\`\`" >> $@ ; \
cat $$file >> $@ ; \
echo "\`\`\`" >> $@ ; \
echo "" >> $@ ; \
done

# Internal dependencies and transformations

$(TREE_FILES): $(SOURCE_FILES)

%.tree: %.t.yaml
@for file in $^; do \
echo "[Convert] $$file -> $${file%.t.yaml}.tree" ; \
cat $$file | $(MAKE_TEST_TREE) > $${file%.t.yaml}.tree ; \
done

# Global

.PHONY: init
init: ## Check the dependencies and prompt to install if needed
@which deno > /dev/null && echo "Deno is available" || echo "Install Deno: curl -fsSL https://deno.land/install.sh | sh"
@which bulloak > /dev/null && echo "bulloak is available" || echo "Install bulloak: cargo install bulloak"

.PHONY: clean
clean: ## Clean the intermediary tree files
rm -f $(TREE_FILES)
rm -f $(TEST_TREE_MARKDOWN)
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -261,3 +261,78 @@ If a some contracts fail to verify on Etherscan, retry with this command:
```shell
forge script --chain "$NETWORK" script/Deploy.s.sol:Deploy --rpc-url "$RPC_URL" --verify --legacy --private-key "$DEPLOYMENT_PRIVATE_KEY" --resume
```

## Testing

See the [test tree](./TEST_TREE.md) file for a visual representation of the implemented tests.

Tests can be described using yaml files. They will be automatically transformed into solidity test files with [bulloak](https://github.com/alexfertel/bulloak).

Create a file with `.t.yaml` extension within the `test` folder and describe a hierarchy of test cases:

```yaml
# MyTest.t.yaml

MultisigTest:
- given: proposal exists
comment: Comment here
and:
- given: proposal is in the last stage
and:

- when: proposal can advance
then:
- it: Should return true

- when: proposal cannot advance
then:
- it: Should return false

- when: proposal is not in the last stage
then:
- it: should do A
comment: This is an important remark
- it: should do B
- it: should do C

- when: proposal doesn't exist
comment: Testing edge cases here
then:
- it: should revert
```
Then use `make` to automatically sync the described branches into solidity test files.

```sh
$ make
Available targets:
Available targets:
- make all Builds all tree files and updates the test tree markdown
- make sync Scaffold or sync tree files into solidity tests
- make check Checks if solidity files are out of sync
- make markdown Generates a markdown file with the test definitions rendered as a tree
- make init Check the dependencies and prompt to install if needed
- make clean Clean the intermediary tree files
$ make sync
```

The final output will look like a human readable tree:

```
# MyTest.tree
EmergencyMultisigTest
├── Given proposal exists // Comment here
│ ├── Given proposal is in the last stage
│ │ ├── When proposal can advance
│ │ │ └── It Should return true
│ │ └── When proposal cannot advance
│ │ └── It Should return false
│ └── When proposal is not in the last stage
│ ├── It should do A // Careful here
│ ├── It should do B
│ └── It should do C
└── When proposal doesn't exist // Testing edge cases here
└── It should revert
```
3 changes: 3 additions & 0 deletions TEST_TREE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Test tree definitions

Below is the graphical definition of the contract tests implemented on [the test folder](./test)
151 changes: 151 additions & 0 deletions test/script/make-test-tree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Run this program with:
// $ cat file.t.yaml | deno run test/script/make-test-tree.ts

import { parse } from "jsr:@std/yaml";

type Rule = {
comment?: string;
given?: string;
when?: string;
and?: Array<Rule>;
then?: Array<Rule>;
it?: string;
};

type TreeItem = {
content: string;
children: Array<TreeItem>;
comment?: string;
};

async function main() {
const content = await readStdinText();
const tree = processTree(content);
Deno.stdout.write(new TextEncoder().encode(tree));
}

function processTree(content: string) {
const data: { [k: string]: Array<Rule> } = parse(content);
if (!data || typeof data !== "object") {
throw new Error("The file format is not a valid yaml object");
}

const rootKeys = Object.keys(data);
if (rootKeys.length > 1) {
throw new Error("The test definition must have only one root node");
}
const [rootKey] = rootKeys;
if (!rootKey || !data[rootKey]) {
throw new Error("A root node needs to be defined");
} else if (!data[rootKey].length) {
throw new Error("The root node needs to include at least one element");
}

const root: TreeItem = {
content: rootKey,
children: parseRuleChildren(data[rootKey]),
};
return renderTree(root);
}

function parseRuleChildren(lines: Array<Rule>): Array<TreeItem> {
if (!lines.length) return [];

const result: Array<TreeItem> = lines.map((rule) => {
if (!rule.when && !rule.given && !rule.it)
throw new Error("All rules should have a 'given', 'when' or 'it' rule");

let content = "";
if (rule.given) {
content = "Given " + rule.given;
} else if (rule.when) {
content = "When " + rule.when;
} else if (rule.it) {
content = "It " + rule.it;
}

let children: TreeItem[] = [];
if (rule.and?.length) {
children = parseRuleChildren(rule.and);
} else if (rule.then?.length) {
children = parseRuleChildren(rule.then);
}

const result: TreeItem = {
content,
children,
};

if (rule.comment) result.comment = rule.comment;
return result;
});

return result;
}

function renderTree(root: TreeItem): string {
let result = root.content + "\n";

for (let i = 0; i < root.children.length; i++) {
const item = root.children[i];
const newLines = renderTreeItem(item, i === root.children.length - 1);
result += newLines.join("\n") + "\n";
}

return result;
}

function renderTreeItem(
root: TreeItem,
lastChildren: boolean,
prefix = ""
): Array<string> {
const result: string[] = [];

// Add ourselves
const content = root.comment
? `${root.content} // ${root.comment}`
: root.content;

if (lastChildren) {
result.push(prefix + "└── " + content);
} else {
result.push(prefix + "├── " + content);
}

// Add any children
for (let i = 0; i < root.children.length; i++) {
const item = root.children[i];

// Last child
if (i === root.children.length - 1) {
const newPrefix = lastChildren ? prefix + " " : prefix + "│ ";
const lines = renderTreeItem(item, true, newPrefix);
lines.forEach((line) => result.push(line));
continue;
}

// The rest of children
const newPrefix = lastChildren ? prefix + " " : prefix + "│ ";
const lines = renderTreeItem(item, false, newPrefix);
lines.forEach((line) => result.push(line));
}

return result;
}

async function readStdinText() {
let result = "";
const decoder = new TextDecoder();
for await (const chunk of Deno.stdin.readable) {
const text = decoder.decode(chunk);
result += text;
}
return result;
}

if (import.meta.main) {
main().catch((err) => {
console.error("Error: " + err.message);
});
}