Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ When you create a new project with `aztec new`, it generates a workspace with tw

The workspace root `Nargo.toml` declares both crates as workspace members. The contract code lives in `contract/src/main.nr`, and tests live in a separate `test` crate that depends on the contract crate.

:::warning Keep tests out of the contract crate
Do not add `#[test]` functions to the `contract` crate. Because the contract artifact depends on everything in its crate, any change — including a test-only change — forces a full recompilation of the contract. The separate `test` crate lets you iterate on tests without rebuilding the contract. See [Testing Contracts](../testing_contracts.md#keep-tests-in-the-test-crate) for details.
:::

See the vanilla Noir docs for [more info on packages](https://noir-lang.org/docs/noir/modules_packages_crates/crates_and_packages).

## Contract block
Expand Down
16 changes: 15 additions & 1 deletion docs/docs-developers/docs/aztec-nr/testing_contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,23 @@ aztec test
Always use `aztec test` instead of `nargo test`. The `TestEnvironment` requires the TXE (Test eXecution Environment) oracle resolver.
:::

## Keep tests in the test crate

When you create a project with `aztec new` or `aztec init`, the generated workspace has two crates: `contract` and `test`. It is important that all tests live in the `test` crate, **not** in the `contract` crate.
Comment thread
benesjan marked this conversation as resolved.

If you place `#[test]` functions inside the contract crate, `aztec compile` will emit a warning:

```
WARNING: Found tests in contract crate(s):
my_contract::test_something
Tests should be in a dedicated test crate, not in the contract crate.
```

The reason is **unnecessary recompilation**: the contract artifact depends on everything inside the contract crate. If tests live there too, editing a test changes the crate and forces the contract to be recompiled and reprocessed, even though the contract logic itself has not changed. By keeping tests in a separate crate, you can iterate on tests without triggering a full contract rebuild.

## Basic test structure

When you create a project with `aztec new` or `aztec init`, a separate `test` crate is created alongside the `contract` crate. Tests live in `test/src/lib.nr` and import the contract crate by name (not `crate::`):
Tests live in `test/src/lib.nr` and import the contract crate by name (not `crate::`):

```rust
use my_contract::MyContract;
Expand Down
7 changes: 6 additions & 1 deletion docs/netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,12 @@
# Add new error codes sequentially (1, 2, 3, ...).
# =============================================================================

[[redirects]]
from = "/errors/1"
Comment thread
benesjan marked this conversation as resolved.
# A warning that is shown when `aztec compile` is run and the contract crate contains tests
to = "/developers/docs/aztec-nr/testing_contracts#keep-tests-in-the-test-crate"

# Example (uncomment and modify when adding error codes):
# [[redirects]]
# from = "/errors/1"
# from = "/errors/2"
# to = "/developers/docs/aztec-nr/framework-description/functions/how_to_define_functions"
19 changes: 19 additions & 0 deletions yarn-project/aztec/src/cli/cmds/compile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,22 @@ function runCodegen() {
throw new Error(`codegen failed:\n${e.stderr?.toString() ?? e.message}`);
}
}

// Validates that aztec compile warns when contract crates contain tests. We want tests to be in a separate `lib` crate.
describe('aztec compile warns about tests in contract crates', () => {
const CONTRACT_WITH_TESTS_WORKSPACE = join(PACKAGE_ROOT, 'test/contract-with-tests');
const CONTRACT_WITH_TESTS_TARGET = join(CONTRACT_WITH_TESTS_WORKSPACE, 'target');

afterAll(() => {
rmSync(CONTRACT_WITH_TESTS_TARGET, { recursive: true, force: true });
});

it('warns when a contract crate contains tests', () => {
const result = execFileSync('node', [CLI, 'compile'], {
cwd: CONTRACT_WITH_TESTS_WORKSPACE,
stdio: 'pipe',
encoding: 'utf-8',
});
expect(result).toMatch(/Found tests in contract crate/);
}, 120_000);
});
104 changes: 104 additions & 0 deletions yarn-project/aztec/src/cli/cmds/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { LogFn } from '@aztec/foundation/log';
import { execFileSync } from 'child_process';
import type { Command } from 'commander';
import { readFile, writeFile } from 'fs/promises';
import { join } from 'path';

import { readArtifactFiles } from './utils/artifacts.js';
import { run } from './utils/spawn.js';
Expand Down Expand Up @@ -34,13 +35,116 @@ async function stripInternalPrefixes(artifactPaths: string[]): Promise<void> {
}
}

/** Returns the set of package names that are contract crates in the current workspace. */
async function getContractPackageNames(): Promise<Set<string>> {
const contractNames = new Set<string>();

let rootToml: string;
try {
rootToml = await readFile('Nargo.toml', 'utf-8');
} catch {
return contractNames;
}

const membersMatch = rootToml.match(/members\s*=\s*\[([^\]]*)\]/);
Comment thread
benesjan marked this conversation as resolved.
if (membersMatch) {
const members = membersMatch[1]
.split(',')
.map(m => m.trim().replace(/^"|"$/g, ''))
.filter(m => m.length > 0);

for (const member of members) {
try {
const memberToml = await readFile(join(member, 'Nargo.toml'), 'utf-8');
if (/type\s*=\s*"contract"/.test(memberToml)) {
const nameMatch = memberToml.match(/name\s*=\s*"([^"]+)"/);
if (nameMatch) {
contractNames.add(nameMatch[1]);
}
}
} catch {
// Member directory might not exist or have no Nargo.toml; skip.
}
}
} else {
// Single-crate project (no workspace): check if the root Nargo.toml itself is a contract.
if (/type\s*=\s*"contract"/.test(rootToml)) {
const nameMatch = rootToml.match(/name\s*=\s*"([^"]+)"/);
if (nameMatch) {
contractNames.add(nameMatch[1]);
}
}
}

return contractNames;
}

/** Checks that no tests exist in contract crates and fails with a helpful message if they do. */
async function checkNoTestsInContracts(nargo: string, log: LogFn): Promise<void> {
const contractPackages = await getContractPackageNames();
if (contractPackages.size === 0) {
return;
}

let output: string;
try {
// We list tests for all the crates in the workspace
output = execFileSync(nargo, ['test', '--list-tests', '--silence-warnings'], {
Comment thread
benesjan marked this conversation as resolved.
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'inherit'],
});
} catch {
// If listing tests fails (e.g. test crate has compile errors), skip the check.
return;
}

// The output of the `nargo test --list-tests` command is as follows:
// ```
// crate_name_1 test_name_1
// crate_name_2 test_name_2
// ...
// crate_name_n test_name_n
// ```
//
// We parse the individual lines and then we check if any contract crate appeared in the parsed output.
const lines = output
.trim()
.split('\n')
.filter(line => line.length > 0);
const testsInContracts: { packageName: string; testName: string }[] = [];

for (const line of lines) {
const spaceIndex = line.indexOf(' ');
if (spaceIndex === -1) {
continue;
}
const packageName = line.substring(0, spaceIndex);
const testName = line.substring(spaceIndex + 1);
if (contractPackages.has(packageName)) {
testsInContracts.push({ packageName, testName });
}
}

if (testsInContracts.length > 0) {
const details = testsInContracts.map(t => ` ${t.packageName}::${t.testName}`).join('\n');
log(
`WARNING: Found tests in contract crate(s):\n${details}\n\n` +
`Tests should be in a dedicated test crate, not in the contract crate.\n` +
`Learn more: https://docs.aztec.network/errors/1`,
);
}
}

/** Compiles Aztec Noir contracts and postprocesses artifacts. */
async function compileAztecContract(nargoArgs: string[], log: LogFn): Promise<void> {
const nargo = process.env.NARGO ?? 'nargo';
const bb = process.env.BB ?? 'bb';

await run(nargo, ['compile', ...nargoArgs]);

// Ensure contract crates contain no tests (tests belong in the test crate).
await checkNoTestsInContracts(nargo, log);

const artifacts = await collectContractArtifacts();

if (artifacts.length > 0) {
Expand Down
1 change: 1 addition & 0 deletions yarn-project/aztec/test/contract-with-tests/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target/
2 changes: 2 additions & 0 deletions yarn-project/aztec/test/contract-with-tests/Nargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[workspace]
members = ["test_contract"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[package]
name = "test_contract"
type = "contract"

[dependencies]
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
contract TestContract {
fn dummy() -> pub Field {
0
}
}

#[test]
fn test_should_not_be_in_contract() {
assert(1 == 1);
}
Loading