extension_tests.rs

  1use gh_workflow::*;
  2use indoc::indoc;
  3
  4use crate::tasks::workflows::{
  5    extension_bump::compare_versions,
  6    run_tests::{fetch_ts_query_ls, orchestrate_for_extension, run_ts_query_ls, tests_pass},
  7    runners,
  8    steps::{
  9        self, BASH_SHELL, CommonJobConditions, FluentBuilder, NamedJob,
 10        cache_rust_dependencies_namespace, named,
 11    },
 12    vars::{PathCondition, StepOutput, WorkflowInput, one_workflow_per_non_main_branch_and_token},
 13};
 14
 15pub(crate) const ZED_EXTENSION_CLI_SHA: &str = "1fa7f1a3ec28ea1eae6db2e937d7a538fb10c0c7";
 16
 17// This should follow the set target in crates/extension/src/extension_builder.rs
 18const EXTENSION_RUST_TARGET: &str = "wasm32-wasip2";
 19
 20// This is used by various extensions repos in the zed-extensions org to run automated tests.
 21pub(crate) fn extension_tests() -> Workflow {
 22    let should_check_rust = PathCondition::new("check_rust", r"^(Cargo.lock|Cargo.toml|.*\.rs)$");
 23    let should_check_extension =
 24        PathCondition::new("check_extension", r"^(extension\.toml|.*\.scm)$");
 25
 26    let orchestrate = with_extension_defaults(orchestrate_for_extension(&[
 27        &should_check_rust,
 28        &should_check_extension,
 29    ]));
 30
 31    let jobs = [
 32        orchestrate,
 33        should_check_rust.guard(check_rust()),
 34        should_check_extension.guard(check_extension()),
 35    ];
 36
 37    let tests_pass = tests_pass(&jobs, &[]);
 38
 39    let working_directory = WorkflowInput::string("working-directory", Some(".".to_owned()));
 40
 41    named::workflow()
 42        .add_event(
 43            Event::default().workflow_call(
 44                WorkflowCall::default()
 45                    .add_input(working_directory.name, working_directory.call_input()),
 46            ),
 47        )
 48        .concurrency(one_workflow_per_non_main_branch_and_token(
 49            "extension-tests",
 50        ))
 51        .add_env(("CARGO_TERM_COLOR", "always"))
 52        .add_env(("RUST_BACKTRACE", 1))
 53        .add_env(("CARGO_INCREMENTAL", 0))
 54        .add_env(("ZED_EXTENSION_CLI_SHA", ZED_EXTENSION_CLI_SHA))
 55        .add_env(("RUSTUP_TOOLCHAIN", "stable"))
 56        .add_env(("CARGO_BUILD_TARGET", EXTENSION_RUST_TARGET))
 57        .map(|workflow| {
 58            jobs.into_iter()
 59                .chain([tests_pass])
 60                .fold(workflow, |workflow, job| {
 61                    workflow.add_job(job.name, job.job)
 62                })
 63        })
 64}
 65
 66fn install_rust_target() -> Step<Run> {
 67    named::bash(format!("rustup target add {EXTENSION_RUST_TARGET}",))
 68}
 69
 70fn get_package_name() -> (Step<Run>, StepOutput) {
 71    let step = named::bash(indoc! {r#"
 72        PACKAGE_NAME="$(sed -n 's/^name = "\(.*\)"/\1/p' < Cargo.toml | head -1 | tr -d '[:space:]')"
 73        echo "package_name=${PACKAGE_NAME}" >> "$GITHUB_OUTPUT"
 74    "#})
 75    .id("get-package-name");
 76
 77    let output = StepOutput::new(&step, "package_name");
 78    (step, output)
 79}
 80
 81fn cargo_fmt_package(package_name: &StepOutput) -> Step<Run> {
 82    named::bash(r#"cargo fmt -p "$PACKAGE_NAME" -- --check"#)
 83        .add_env(("PACKAGE_NAME", package_name.to_string()))
 84}
 85
 86fn run_clippy(package_name: &StepOutput) -> Step<Run> {
 87    named::bash(r#"cargo clippy -p "$PACKAGE_NAME" --release --all-features -- --deny warnings"#)
 88        .add_env(("PACKAGE_NAME", package_name.to_string()))
 89}
 90
 91fn run_nextest(package_name: &StepOutput) -> Step<Run> {
 92    named::bash(
 93        r#"cargo nextest run -p "$PACKAGE_NAME" --no-fail-fast --no-tests=warn --target "$(rustc -vV | sed -n 's|host: ||p')""#,
 94    )
 95    .add_env(("PACKAGE_NAME", package_name.to_string()))
 96    .add_env(("NEXTEST_NO_TESTS", "warn"))
 97}
 98
 99fn extension_job_defaults() -> Defaults {
100    Defaults::default().run(
101        RunDefaults::default()
102            .shell(BASH_SHELL)
103            .working_directory("${{ inputs.working-directory }}"),
104    )
105}
106
107fn with_extension_defaults(named_job: NamedJob) -> NamedJob {
108    NamedJob {
109        name: named_job.name,
110        job: named_job.job.defaults(extension_job_defaults()),
111    }
112}
113
114fn check_rust() -> NamedJob {
115    let (get_package, package_name) = get_package_name();
116
117    let job = Job::default()
118        .defaults(extension_job_defaults())
119        .with_repository_owner_guard()
120        .runs_on(runners::LINUX_LARGE_RAM)
121        .timeout_minutes(6u32)
122        .add_step(steps::checkout_repo())
123        .add_step(steps::cache_rust_dependencies_namespace())
124        .add_step(install_rust_target())
125        .add_step(get_package)
126        .add_step(cargo_fmt_package(&package_name))
127        .add_step(run_clippy(&package_name))
128        .add_step(steps::cargo_install_nextest())
129        .add_step(run_nextest(&package_name));
130
131    named::job(job)
132}
133
134pub(crate) fn check_extension() -> NamedJob {
135    let (cache_download, cache_hit) = cache_zed_extension_cli();
136    let (check_version_job, version_changed, _) = compare_versions();
137
138    let job = Job::default()
139        .defaults(extension_job_defaults())
140        .with_repository_owner_guard()
141        .runs_on(runners::LINUX_LARGE_RAM)
142        .timeout_minutes(6u32)
143        .add_step(steps::checkout_repo().with_full_history())
144        .add_step(cache_download)
145        .add_step(download_zed_extension_cli(cache_hit))
146        .add_step(cache_rust_dependencies_namespace()) // Extensions can compile Rust, so provide the cache if needed.
147        .add_step(check())
148        .add_step(fetch_ts_query_ls())
149        .add_step(run_ts_query_ls())
150        .add_step(check_version_job)
151        .add_step(verify_version_did_not_change(version_changed));
152
153    named::job(job)
154}
155
156pub fn cache_zed_extension_cli() -> (Step<Use>, StepOutput) {
157    let step = named::uses(
158        "actions",
159        "cache",
160        "0057852bfaa89a56745cba8c7296529d2fc39830",
161    )
162    .id("cache-zed-extension-cli")
163    .with(
164        Input::default()
165            .add("path", "zed-extension")
166            .add("key", "zed-extension-${{ env.ZED_EXTENSION_CLI_SHA }}"),
167    );
168    let output = StepOutput::new(&step, "cache-hit");
169    (step, output)
170}
171
172pub fn download_zed_extension_cli(cache_hit: StepOutput) -> Step<Run> {
173    named::bash(
174    indoc! {
175        r#"
176        wget --quiet "https://zed-extension-cli.nyc3.digitaloceanspaces.com/$ZED_EXTENSION_CLI_SHA/x86_64-unknown-linux-gnu/zed-extension" -O "$GITHUB_WORKSPACE/zed-extension"
177        chmod +x "$GITHUB_WORKSPACE/zed-extension"
178        "#,
179    }
180    ).if_condition(Expression::new(format!("{} != 'true'", cache_hit.expr())))
181}
182
183pub fn check() -> Step<Run> {
184    named::bash(indoc! {
185        r#"
186        mkdir -p /tmp/ext-scratch
187        mkdir -p /tmp/ext-output
188        "$GITHUB_WORKSPACE/zed-extension" --source-dir . --scratch-dir /tmp/ext-scratch --output-dir /tmp/ext-output
189        "#
190    })
191}
192
193fn verify_version_did_not_change(version_changed: StepOutput) -> Step<Run> {
194    named::bash(indoc! {r#"
195        if [[ "$VERSION_CHANGED" == "true" && "$GITHUB_EVENT_NAME" == "pull_request" && "$PR_USER_LOGIN" != "zed-zippy[bot]" ]] ; then
196            echo "Version change detected in your change!"
197            echo "Version changes happen in separate PRs and will be performed by the zed-zippy bot"
198            exit 42
199        fi
200        "#
201    })
202    .add_env(("VERSION_CHANGED", version_changed.to_string()))
203    .add_env(("PR_USER_LOGIN", "${{ github.event.pull_request.user.login }}"))
204}