Skip to content

Commit

Permalink
Sway unit test (i.e. #[test] fn) compilation (FuelLabs#2985)
Browse files Browse the repository at this point in the history
OK, this should be good to go!

## Highlights

- Adds support for multiple entry points to IR. Adds a tests for
serializing entry functions to/from IR.
- Enables ASM generation for libraries (to support test function entry
points).
- Track entry points through ASM generation so we can return entry point
metadata as a part of the compilation result. This doesn't affect ASM
generation, but allows tools using `sway-core` as a library to work with
different entry points.
- Updated E2E test harness with a new test category "UnitTestsPass".
- Added E2E tests with multiple unit tests for each type of Sway program
(library, script, predicate, contract).
- Gets `forc test` working with pretty output!

Here's the output from the new `lib_multi_test`:

![Screenshot from 2022-11-03
19-00-57](https://user-images.githubusercontent.com/4587373/199700362-ba32f90d-1f0f-4f76-bf89-b191de9589af.png)


## Test running implementation

Currently, it seems like there's no publicly accessible approach to
execute a script directly from a custom entry point. As a result, for
each test, we patch the bytecode with a `JI` instruction that jumps from
after the data section setup to the test's entry point. This is a bit
hairy, but works for now!

As a follow-up, we may want to consider adding support upstream in
`fuel-vm` for executing scripts directly from a given entry point. Even
if this was only exposed from the interpreter API, this would make the
`forc-test` implementation quite a bit cleaner and avoid the need to
patch the bytecode.

Alternatively, when building projects we could return more metadata
along with the compiled output (whether in memory or bytecode header) to
indicate how to work with different entry points in a more reliable
manner (rather than the magic const offset currently used in
`forc-test`).

# TODO

- [x] Add `include_tests` flag to `BuildConfig`, allowing `forc` to
trigger compilation of test functions.
- [x] Include `#[test]` fns as entry points within dead code analysis.
- [x] IR and ASM generation for test entry points.
- [x] Add `forc build --tests`.
- [x] Add `forc test`.
- [x] Always include test fns in `TyProgram` (for DCA), but omit from IR
if not building tests.
- [x] Work out how to iterate over different entry points during `forc
test`.
- [x] Only generate tests for top-level "members" (not all
dependencies).

### Follow-up:

- FuelLabs#3260
- FuelLabs#3261
- FuelLabs#3262
- FuelLabs#3263
- FuelLabs#3264
- FuelLabs#3265
- FuelLabs#3266
- FuelLabs#3267
- FuelLabs#3268

Closes FuelLabs#1832.
  • Loading branch information
mitchmindtree authored Nov 4, 2022
1 parent 3805a6f commit 5f29c66
Show file tree
Hide file tree
Showing 54 changed files with 920 additions and 295 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ jobs:
./.github/workflows/scripts/verify_tag.sh ${{ github.ref_name }} forc-plugins/forc-doc/Cargo.toml
./.github/workflows/scripts/verify_tag.sh ${{ github.ref_name }} forc-plugins/forc-fmt/Cargo.toml
./.github/workflows/scripts/verify_tag.sh ${{ github.ref_name }} forc-plugins/forc-lsp/Cargo.toml
./.github/workflows/scripts/verify_tag.sh ${{ github.ref_name }} forc-test/Cargo.toml
./.github/workflows/scripts/verify_tag.sh ${{ github.ref_name }} forc-tracing/Cargo.toml
./.github/workflows/scripts/verify_tag.sh ${{ github.ref_name }} forc-util/Cargo.toml
./.github/workflows/scripts/verify_tag.sh ${{ github.ref_name }} sway-ast/Cargo.toml
Expand Down
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ members = [
"forc-plugins/forc-doc",
"forc-plugins/forc-fmt",
"forc-plugins/forc-lsp",
"forc-test",
"forc-tracing",
"forc-util",
"scripts/examples-checker",
Expand Down
85 changes: 26 additions & 59 deletions forc-pkg/src/pkg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
use anyhow::{anyhow, bail, Context, Error, Result};
use forc_util::{
default_output_directory, find_file_name, git_checkouts_directory, kebab_to_snake_case,
print_on_failure, print_on_success, print_on_success_library,
print_on_failure, print_on_success,
};
use petgraph::{
self,
Expand All @@ -36,7 +36,7 @@ use sway_core::{
},
semantic_analysis::namespace,
source_map::SourceMap,
CompileResult, CompiledBytecode,
CompileResult, CompiledBytecode, FinalizedEntry,
};
use sway_error::error::CompileError;
use sway_types::{Ident, JsonABIProgram, JsonTypeApplication, JsonTypeDeclaration};
Expand Down Expand Up @@ -78,11 +78,12 @@ pub struct BuiltPackage {
pub json_abi_program: JsonABIProgram,
pub storage_slots: Vec<StorageSlot>,
pub bytecode: Vec<u8>,
pub entries: Vec<FinalizedEntry>,
pub tree_type: TreeType,
}

pub enum Built {
Package(BuiltPackage),
Package(Box<BuiltPackage>),
Workspace,
}

Expand Down Expand Up @@ -2009,7 +2010,7 @@ pub fn compile(
build_profile: &BuildProfile,
namespace: namespace::Module,
source_map: &mut SourceMap,
) -> Result<(BuiltPackage, Option<namespace::Root>)> {
) -> Result<(BuiltPackage, namespace::Root)> {
// Time the given expression and print the result if `build_config.time_phases` is true.
macro_rules! time_expr {
($description:expr, $expression:expr) => {{
Expand Down Expand Up @@ -2061,33 +2062,22 @@ pub fn compile(

let storage_slots = typed_program.storage_slots.clone();
let tree_type = typed_program.kind.tree_type();
match tree_type {
// On errors, do not proceed to compiling bytecode, as semantic analysis did not pass.
_ if !ast_res.errors.is_empty() => {
return fail(&ast_res.warnings, &ast_res.errors);
}
// If we're compiling a library, we don't need to compile any further.
// Instead, we update the namespace with the library's top-level module.
TreeType::Library { .. } => {
print_on_success_library(terse_mode, &pkg.name, &ast_res.warnings);
let bytecode = vec![];
let lib_namespace = typed_program.root.namespace.clone();
let built_package = BuiltPackage {
json_abi_program,
storage_slots,
bytecode,
tree_type,
};
return Ok((built_package, Some(lib_namespace.into())));
}
// For all other program types, we'll compile the bytecode.
TreeType::Contract | TreeType::Predicate | TreeType::Script => {}

let namespace = typed_program.root.namespace.clone().into();

if !ast_res.errors.is_empty() {
return fail(&ast_res.warnings, &ast_res.errors);
}

let asm_res = time_expr!(
"compile ast to asm",
sway_core::ast_to_asm(ast_res, &sway_build_config)
);
let entries = asm_res
.value
.as_ref()
.map(|asm| asm.0.entries.clone())
.unwrap_or_default();
let bc_res = time_expr!(
"compile asm to bytecode",
sway_core::asm_to_bytecode(asm_res, source_map)
Expand All @@ -2102,8 +2092,9 @@ pub fn compile(
storage_slots,
bytecode,
tree_type,
entries,
};
Ok((built_package, None))
Ok((built_package, namespace))
}
_ => fail(&bc_res.warnings, &bc_res.errors),
}
Expand Down Expand Up @@ -2272,7 +2263,7 @@ pub fn build_with_options(build_options: BuildOpts) -> Result<Built> {
match manifest_file {
ManifestFile::Package(package_manifest) => {
let built_package = build_package_with_options(&package_manifest, build_options)?;
Ok(Built::Package(built_package))
Ok(Built::Package(Box::new(built_package)))
}
ManifestFile::Workspace(_) => bail!("Workspace building is not supported"),
}
Expand Down Expand Up @@ -2303,15 +2294,8 @@ pub fn build(

let mut lib_namespace_map = Default::default();
let mut source_map = SourceMap::new();
let mut json_abi_program = JsonABIProgram {
types: vec![],
functions: vec![],
logged_types: vec![],
};
let mut storage_slots = vec![];
let mut bytecode = vec![];
let mut tree_type = None;
let mut compiled_contract_deps = HashMap::new();
let mut last_pkg = None;
for &node in &plan.compilation_order {
let pkg = &plan.graph()[node];
let manifest = &plan.manifest_map()[&pkg.id()];
Expand All @@ -2330,7 +2314,7 @@ pub fn build(
}
};
let res = compile(pkg, manifest, profile, dep_namespace, &mut source_map)?;
let (built_package, maybe_namespace) = res;
let (built_package, namespace) = res;
// If the current node is a contract dependency, collect the contract_id
if plan
.graph()
Expand All @@ -2339,35 +2323,18 @@ pub fn build(
{
compiled_contract_deps.insert(node, built_package.clone());
}
if let Some(namespace) = maybe_namespace {
if let TreeType::Library { .. } = built_package.tree_type {
lib_namespace_map.insert(node, namespace.into());
}
json_abi_program
.types
.extend(built_package.json_abi_program.types);
json_abi_program
.functions
.extend(built_package.json_abi_program.functions);
json_abi_program
.logged_types
.extend(built_package.json_abi_program.logged_types);
storage_slots.extend(built_package.storage_slots);
bytecode = built_package.bytecode;
tree_type = Some(built_package.tree_type);
last_pkg = Some(built_package);
source_map.insert_dependency(manifest.dir());
}

standardize_json_abi_types(&mut json_abi_program);
let mut built_pkg =
last_pkg.ok_or_else(|| anyhow!("build plan must contain at least one package"))?;
standardize_json_abi_types(&mut built_pkg.json_abi_program);

let tree_type =
tree_type.ok_or_else(|| anyhow!("build plan must contain at least one package"))?;
let built_package = BuiltPackage {
bytecode,
json_abi_program,
storage_slots,
tree_type,
};
Ok((built_package, source_map))
Ok((built_pkg, source_map))
}

/// Standardize the JSON ABI data structure by eliminating duplicate types. This is an iterative
Expand Down
17 changes: 17 additions & 0 deletions forc-test/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "forc-test"
version = "0.30.0"
authors = ["Fuel Labs <[email protected]>"]
edition = "2021"
homepage = "https://fuel.network/"
license = "Apache-2.0"
repository = "https://github.com/FuelLabs/sway"
description = "A library for building and running Sway unit tests within Forc packages."

[dependencies]
anyhow = "1"
forc-pkg = { version = "0.30.0", path = "../forc-pkg" }
fuel-tx = { version = "0.23", features = ["builder"] }
fuel-vm = { version = "0.22", features = ["random"] }
rand = "0.8"
sway-core = { version = "0.30.0", path = "../sway-core" }
Loading

0 comments on commit 5f29c66

Please sign in to comment.