Rakko

Test an Action

Test outcomes and make sure that read-only runs leave project files unchanged.

Test your action in a temporary project and inspect its outcome. The rakko-test-utils helpers also detect file changes when a run declares that it only reads the project.

This guide tests check-readme from Build an Action. You need an action to test, but no harness.

Add the Test Dependencies

In your action crate, add rakko-test-utils, tempfile for temporary projects, and Tokio to run asynchronous tests:

cargo add --dev rakko-test-utils tempfile
cargo add --dev tokio --features macros,rt

Choose a release of rakko-test-utils that uses the same release of rakko as your action. Otherwise, the helpers expect a different Action trait and the tests do not compile.

Test a Run

Put the tests in tests/, such as tests/check_readme.rs. Start the file with these imports:

use std::fs;

use otter_check_readme::{CheckReadme, CheckReadmeArgs};
use rakko::{Access, Action, Args, ArgsValues, ArgumentValue, Context, Outcome, argument_name};
use rakko_test_utils::run_checking_access;

A test creates a project in a temporary directory, runs the action there, and checks the outcome:

#[tokio::test]
async fn run_without_readme_fails() {
    let project = tempfile::tempdir().unwrap();
    let context = Context::builder().root(project.path()).build();

    let outcome = run_checking_access(&CheckReadme, &context, &CheckReadmeArgs::default()).await;

    assert!(matches!(outcome, Outcome::Failed { .. }));
}

Context uses the temporary directory as the project root. run_checking_access runs the action with your context and arguments, then returns its outcome. The action is asynchronous, so #[tokio::test] supplies the runtime that the test needs.

To test a project that passes, create its files before the run:

#[tokio::test]
async fn run_with_readme_passes() {
    let project = tempfile::tempdir().unwrap();
    fs::write(project.path().join("README.md"), "# Otter\n").unwrap();
    let context = Context::builder().root(project.path()).build();

    let outcome = run_checking_access(&CheckReadme, &context, &CheckReadmeArgs::default()).await;

    assert!(matches!(outcome, Outcome::Passed { .. }));
}

Test With Arguments

The fields of CheckReadmeArgs are private. Use from_values to construct arguments through the same interface as the command line:

/// Returns the values of a run with `--fix`, as the command line gives them
fn values_with_fix() -> ArgsValues {
    ArgsValues::new([(argument_name!("fix"), ArgumentValue::new("true"))])
}

A boolean flag that the user gave has the value true. The test then runs the action with these arguments:

#[tokio::test]
async fn run_with_fix_writes_readme() {
    let project = tempfile::tempdir().unwrap();
    let context = Context::builder().root(project.path()).build();
    let args = CheckReadmeArgs::from_values(&values_with_fix()).unwrap();

    run_checking_access(&CheckReadme, &context, &args).await;

    assert!(project.path().join("README.md").exists());
}

Test access directly. The method reads no files and starts no tools:

#[test]
fn access_with_fix_is_write() {
    let project = tempfile::tempdir().unwrap();
    let context = Context::builder().root(project.path()).build();
    let args = CheckReadmeArgs::from_values(&values_with_fix()).unwrap();

    let access = CheckReadme.access(&context, &args);

    assert_eq!(access, Access::Write);
}

The Access Check

run_checking_access reads the access declaration before it runs the action. For a read-only run, it compares project files before and after the run. Added, removed, or changed files fail the test, and the message lists them. For a run that declares a write, the helper returns the outcome without comparing files.

The comparison excludes these files and directories at every depth:

  • Directories named .git.
  • Directories named target.
  • Files whose names end in .tsbuildinfo.

The harness trusts access declarations and can run readers at the same time. If an action writes after declaring a read, it can change another action’s input. Use this helper to catch those changes in tests.

Test an Action That Runs a Tool

A test can inspect a tool declaration without installing the tool:

use otter_check_toml::CheckToml;
use rakko::{Action, RequiredVersion, ToolVersion};
use rakko_tool::MiseFile;

#[test]
fn tools_pins_the_version_of_the_mise_file() {
    let pin = MiseFile::new(include_str!("../mise.rakko.toml")).pinned_version("taplo");

    let tools = CheckToml.tools();

    assert_eq!(
        tools[0].version(),
        &RequiredVersion::Exact(ToolVersion::new(pin))
    );
}

This test reads the version from the same mise file as the action. So it still passes when Renovate updates the pin.

A test that runs the tool needs the tool at the version that your crate pins. Install it in the root of the crate before you run the tests:

MISE_ENV=rakko mise install

run_checking_access calls your action directly and bypasses the platform skip. If the tool has no build for the test platform, the action returns an error. Limit tests that run the tool to its supported platforms. Run a Tool From an Action explains platform declarations.

Compare Paths

Windows uses different path separators from macOS and Linux. Use path to convert a literal such as src/lib.rs to the platform’s path format. Use path_text when you need that path as text, for example in an expected tool report.

Run the Tests

Run the tests from your action crate:

cargo test