Repository: railwayapp/nixpacks
Pull request #403 (2022-08-14 14:43:27)

Add a macro to help with plan tests
## What does this PR address?
<!-- Thanks for sending a pull request! -->

Creates a separate helper crate to remove code duplication on plan tests. 

## Before submitting:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask -->
<!--- If you plan to update documentation or tests in follow-up, please note -->

- [x] Make sure to follow [GitHub's guide](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) on creating PR.
- [x] Did you read through [contribution guidelines](https://github.com/railwayapp/nixpacks/blob/main/CONTRIBUTING.md)?
- [ ] Did your changes require updates to the documentation? Have you updated those accordingly?
- [x] Did you write tests to cover your changes? Did it pass locally when running `cargo test`?
- [x] Have you run `cargo fmt`, `cargo check` and `cargo clippy` locally to ensure that CI passes?



Patch:
diff --git a/Cargo.lock b/Cargo.lock
index f9d878b..45e6f08 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -145,9 +145,9 @@ dependencies = [
 
 [[package]]
 name = "console"
-version = "0.15.0"
+version = "0.15.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31"
+checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847"
 dependencies = [
  "encode_unicode",
  "libc",
@@ -200,6 +200,16 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "dotenv-parser"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d4e7e3d829e699806e8f1cb1906e671114c2b955a9c65559756b5f93258e8f0"
+dependencies = [
+ "pest",
+ "pest_derive",
+]
+
 [[package]]
 name = "encode_unicode"
 version = "0.3.6"
@@ -253,6 +263,7 @@ dependencies = [
  "aho-corasick",
  "bstr",
  "fnv",
+ "log",
  "regex",
 ]
 
@@ -295,18 +306,18 @@ checksum = "05a0bd019339e5d968b37855180087b7b9d512c5046fbd244cf8c95687927d6e"
 
 [[package]]
 name = "insta"
-version = "1.16.0"
+version = "1.18.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36fb7ec420af04ce7d1a422945cd19c52bf01772ead45934cee77f056dca1081"
+checksum = "57a9aec10c9a062ef0454fd49ebaefa59239f836d1b30891d9cc2289978dd970"
 dependencies = [
  "console",
+ "linked-hash-map",
  "once_cell",
  "pest",
  "pest_derive",
  "serde",
- "serde_json",
- "serde_yaml",
  "similar",
+ "yaml-rust",
 ]
 
 [[package]]
@@ -333,6 +344,15 @@ version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
 
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
 [[package]]
 name = "maplit"
 version = "1.0.2"
@@ -356,6 +376,7 @@ dependencies = [
  "clap",
  "colored",
  "console",
+ "dotenv-parser",
  "globset",
  "indoc",
  "insta",
@@ -365,6 +386,7 @@ dependencies = [
  "serde_with",
  "serde_yaml",
  "tempdir",
+ "test-helper",
  "textwrap",
  "toml",
  "uuid",
@@ -435,18 +457,18 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.41"
+version = "1.0.43"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdcc2916cde080c1876ff40292a396541241fe0072ef928cd76582e9ea5d60d2"
+checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
 dependencies = [
  "unicode-ident",
 ]
 
 [[package]]
 name = "quote"
-version = "1.0.20"
+version = "1.0.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
 dependencies = [
  "proc-macro2",
 ]
@@ -649,6 +671,16 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "test-helper"
+version = "0.1.0"
+dependencies = [
+ "globset",
+ "proc-macro2",
+ "quote",
+ "walkdir",
+]
+
 [[package]]
 name = "textwrap"
 version = "0.15.0"
diff --git a/Cargo.toml b/Cargo.toml
index ca308b7..fc9d1df 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,6 +18,9 @@ path = "src/main.rs"
 name = "nixpacks"
 path = "src/lib.rs"
 
+[workspace]
+members = ["test-helper"]
+
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
@@ -43,4 +46,6 @@ textwrap = { default-features = false, version = "0.15.0" }
 cargo_toml = "0.11.5"
 
 [dev-dependencies]
-insta = {version = "1.16.0", features=["redactions"]}
+dotenv-parser = "0.1.3"
+insta = { version = "1.18.2", features = ["redactions", "json"] }
+test-helper = { path = "./test-helper" }
diff --git a/examples/config-from-environment-variables/start.sh b/examples/config-from-environment-variables/start.sh
new file mode 100755
index 0000000..c6fd45c
--- /dev/null
+++ b/examples/config-from-environment-variables/start.sh
@@ -0,0 +1,1 @@
+echo 'Hello World' | cowsay
diff --git a/examples/custom-pkgs/start.sh b/examples/custom-pkgs/start.sh
new file mode 100755
index 0000000..c6fd45c
--- /dev/null
+++ b/examples/custom-pkgs/start.sh
@@ -0,0 +1,1 @@
+echo 'Hello World' | cowsay
diff --git a/examples/go-cgo-enabled/main.go b/examples/go-cgo-enabled/main.go
new file mode 100644
index 0000000..9ca9302
--- /dev/null
+++ b/examples/go-cgo-enabled/main.go
@@ -0,0 +1,7 @@
+package main
+
+import "fmt"
+
+func main() {
+	fmt.Println("Hello from Go!")
+}
diff --git a/examples/overriding-environment-variables/index.js b/examples/overriding-environment-variables/index.js
new file mode 100644
index 0000000..b808d1e
--- /dev/null
+++ b/examples/overriding-environment-variables/index.js
@@ -0,0 +1,2 @@
+console.log("--- Environment Variables ---");
+console.log(process.env);
diff --git a/examples/overriding-environment-variables/package.json b/examples/overriding-environment-variables/package.json
new file mode 100644
index 0000000..54b7b47
--- /dev/null
+++ b/examples/overriding-environment-variables/package.json
@@ -0,0 +1,12 @@
+{
+  "name": "variables",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC"
+}
diff --git a/examples/pin_archive/start.sh b/examples/pin_archive/start.sh
new file mode 100755
index 0000000..c6fd45c
--- /dev/null
+++ b/examples/pin_archive/start.sh
@@ -0,0 +1,1 @@
+echo 'Hello World' | cowsay


Test patch:
diff --git a/examples/config-from-environment-variables/test.env b/examples/config-from-environment-variables/test.env
new file mode 100644
index 0000000..7c5e53a
--- /dev/null
+++ b/examples/config-from-environment-variables/test.env
@@ -0,0 +1,3 @@
+# Note: You do NOT need this for your project. This file is only used for testing purposes.
+
+ENVS='NIXPACKS_PKGS=cowsay ripgrep, NIXPACKS_INSTALL_CMD=install, NIXPACKS_BUILD_CMD=build, NIXPACKS_START_CMD=start, NIXPACKS_RUN_IMAGE=alpine, NIXPACKS_INSTALL_CACHE_DIRS=/tmp,foobar, NIXPACKS_BUILD_CACHE_DIRS=/build,barbaz'
\ No newline at end of file
diff --git a/examples/custom-pkgs/test.env b/examples/custom-pkgs/test.env
new file mode 100644
index 0000000..1c3e7ff
--- /dev/null
+++ b/examples/custom-pkgs/test.env
@@ -0,0 +1,4 @@
+# Note: You do NOT need this for your project. This file is only used for testing purposes.
+
+CUSTOM_START_CMD="./start.sh"
+CUSTOM_PKGS=cowsay
diff --git a/examples/go-cgo-enabled/test.env b/examples/go-cgo-enabled/test.env
new file mode 100644
index 0000000..ed74cfa
--- /dev/null
+++ b/examples/go-cgo-enabled/test.env
@@ -0,0 +1,3 @@
+# Note: You do NOT need this for your project. This file is only used for testing purposes.
+
+ENVS="CGO_ENABLED=1"
diff --git a/examples/overriding-environment-variables/test.env b/examples/overriding-environment-variables/test.env
new file mode 100644
index 0000000..9a2e4b6
--- /dev/null
+++ b/examples/overriding-environment-variables/test.env
@@ -0,0 +1,3 @@
+# Note: You do NOT need this for your project. This file is only used for testing purposes.
+
+ENVS="NODE_ENV=test"
diff --git a/examples/pin_archive/test.env b/examples/pin_archive/test.env
new file mode 100644
index 0000000..524e64e
--- /dev/null
+++ b/examples/pin_archive/test.env
@@ -0,0 +1,3 @@
+# Note: You do NOT need this for your project. This file is only used for testing purposes.
+
+PIN_PKGS=1
diff --git a/test-helper/Cargo.toml b/test-helper/Cargo.toml
new file mode 100644
index 0000000..e2207cb
--- /dev/null
+++ b/test-helper/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "test-helper"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+proc-macro = true
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+globset = "0.4.9"
+proc-macro2 = "1.0.43"
+quote = "1.0.21"
+walkdir = "2.3.2"
diff --git a/test-helper/src/lib.rs b/test-helper/src/lib.rs
new file mode 100644
index 0000000..33d1a51
--- /dev/null
+++ b/test-helper/src/lib.rs
@@ -0,0 +1,99 @@
+use proc_macro::TokenStream;
+use quote::{format_ident, quote};
+use walkdir::{DirEntry, WalkDir};
+
+const IGNORE: &[&str] = &[
+    "rust-custom-version",
+    "rust-rocket",
+    "haskell-stack",
+    "zig-gyro",
+    "rust-ring",
+    "rust-openssl",
+    "rust-custom-toolchain",
+    "ruby-no-version",
+];
+
+fn get_examples() -> Vec<String> {
+    let mut current_dir = std::env::current_dir().unwrap();
+
+    current_dir.push("examples");
+
+    let walker = WalkDir::new(&current_dir).max_depth(1);
+
+    walker
+        .sort_by_file_name()
+        .into_iter()
+        .filter_map(Result::ok)
+        .map(DirEntry::into_path)
+        .filter(|path| path.is_dir())
+        .map(|path| path.file_name().unwrap().to_string_lossy().to_string())
+        .filter(|path| !IGNORE.contains(&path.as_str()))
+        .collect()
+}
+
+#[proc_macro]
+pub fn generate_plan_tests(_tokens: TokenStream) -> TokenStream {
+    let mut examples = get_examples();
+    let mut tests = Vec::with_capacity(examples.len());
+
+    // First element is always "examples"
+    examples.remove(0);
+
+    tests.push(quote! {
+        macro_rules! assert_plan_snapshot {
+            ($plan:expr) => {
+                ::insta::assert_json_snapshot!($plan, {
+                    ".version" => "[version]",
+                    ".setup.archive" => "[archive]",
+                    ".setup.baseImage" => "[baseImage]",
+                });
+            }
+        }
+
+        fn simple_gen_plan(path: &str) -> ::nixpacks::nixpacks::plan::BuildPlan {
+            if let Ok(raw_env) = ::std::fs::read_to_string(format!("{}/test.env", path)) {
+                let env = ::dotenv_parser::parse_dotenv(&raw_env).unwrap();
+                let opts = ::nixpacks::nixpacks::plan::generator::GeneratePlanOptions {
+                    pin_pkgs: env.get("PIN_PKGS").is_some(),
+                    custom_start_cmd: env.get("CUSTOM_START_CMD").map(|cmd| cmd.to_string()),
+                    custom_pkgs: env
+                        .get("CUSTOM_PKGS")
+                        .map(|pkgs| pkgs.split(',')
+                        .map(|pkg| ::nixpacks::nixpacks::nix::pkg::Pkg::new(pkg)).collect())
+                        .unwrap_or_default(),
+                    ..::nixpacks::nixpacks::plan::generator::GeneratePlanOptions::default()
+                };
+
+                return ::nixpacks::generate_build_plan(
+                    path,
+                    env.get("ENVS").map(|envs| envs.split(", ").collect()).unwrap_or_default(),
+                    &opts
+                ).unwrap();
+            }
+
+            ::nixpacks::generate_build_plan(
+                path,
+                ::std::vec::Vec::new(),
+                &::nixpacks::nixpacks::plan::generator::GeneratePlanOptions::default()
+            ).unwrap()
+        }
+    });
+
+    for example in examples {
+        let test_name = format_ident!("{}", example.replace('-', "_"));
+        let test = quote! {
+            #[test]
+            fn #test_name() {
+                let plan = simple_gen_plan(&format!("./examples/{}", #example));
+                assert_plan_snapshot!(plan);
+            }
+        };
+
+        tests.push(test);
+    }
+
+    tests
+        .into_iter()
+        .collect::<proc_macro2::TokenStream>()
+        .into()
+}
diff --git a/tests/generate_plan_tests.rs b/tests/generate_plan_tests.rs
index 94cfe86..e926bea 100644
--- a/tests/generate_plan_tests.rs
+++ b/tests/generate_plan_tests.rs
@@ -1,215 +1,7 @@
-use nixpacks::{
-    generate_build_plan,
-    nixpacks::{
-        nix::pkg::Pkg,
-        plan::{generator::GeneratePlanOptions, BuildPlan},
-    },
-};
+use nixpacks::{generate_build_plan, nixpacks::plan::generator::GeneratePlanOptions};
 use std::env::consts::ARCH;
 
-macro_rules! assert_plan_snapshot {
-    ($plan:expr) => {
-        insta::assert_json_snapshot!($plan, {
-            ".version" => "[version]",
-            ".setup.archive" => "[archive]",
-            ".setup.baseImage" => "[baseImage]",
-        });
-    }
-}
-
-fn simple_gen_plan(path: &str) -> BuildPlan {
-    generate_build_plan(path, Vec::new(), &GeneratePlanOptions::default()).unwrap()
-}
-
-#[test]
-fn test_node() {
-    let plan = simple_gen_plan("./examples/node");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_node_no_lockfile() {
-    let plan = simple_gen_plan("./examples/node-no-lockfile-canvas");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_node_npm_old_lockfile() {
-    let plan = simple_gen_plan("./examples/node-npm-old-lockfile");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_npm() {
-    let plan = simple_gen_plan("./examples/node-npm");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_node_no_scripts() {
-    let plan = simple_gen_plan("./examples/node-no-scripts");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_node_custom_version() {
-    let plan = simple_gen_plan("./examples/node-custom-version");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_node_monorepo() {
-    let plan = simple_gen_plan("./examples/node-monorepo");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_yarn() {
-    let plan = simple_gen_plan("./examples/node-yarn");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_yarn_berry() {
-    let plan = simple_gen_plan("./examples/node-yarn-berry");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_yarn_custom_version() {
-    let plan = simple_gen_plan("./examples/node-yarn-custom-node-version");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_pnpm() {
-    let plan = simple_gen_plan("./examples/node-pnpm");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_bun() {
-    let plan = simple_gen_plan("./examples/node-bun");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_bun_no_start() {
-    let plan = simple_gen_plan("./examples/node-bun-no-start");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_bun_web_server() {
-    let plan = simple_gen_plan("./examples/node-bun-no-start");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_pnpm_v7() {
-    let plan = simple_gen_plan("./examples/node-pnpm-v7");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_pnpm_custom_version() {
-    let plan = simple_gen_plan("./examples/node-pnpm-custom-node-version");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_go() {
-    let plan = simple_gen_plan("./examples/go");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_go_cgo_enabled() {
-    let plan = generate_build_plan(
-        "./examples/go",
-        vec!["CGO_ENABLED=1"],
-        &GeneratePlanOptions::default(),
-    )
-    .unwrap();
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_go_mod() {
-    let plan = simple_gen_plan("./examples/go-mod");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_go_custom_version() {
-    let plan = simple_gen_plan("./examples/go-custom-version");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_deno() {
-    let plan = simple_gen_plan("./examples/deno");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_deno_fresh() {
-    let plan = simple_gen_plan("./examples/deno-fresh");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_csharp_api() {
-    let plan = simple_gen_plan("./examples/csharp-api");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_fsharp_api() {
-    let plan = simple_gen_plan("./examples/fsharp-api");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_csharp_cli() {
-    let plan = simple_gen_plan("./examples/csharp-cli");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_procfile() {
-    let plan = simple_gen_plan("./examples/procfile");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_custom_pkgs() {
-    let plan = generate_build_plan(
-        "./examples/shell-hello",
-        Vec::new(),
-        &GeneratePlanOptions {
-            custom_start_cmd: Some("./start.sh".to_string()),
-            custom_pkgs: vec![Pkg::new("cowsay")],
-            ..Default::default()
-        },
-    )
-    .unwrap();
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_pin_archive() {
-    let plan = generate_build_plan(
-        "./examples/shell-hello",
-        Vec::new(),
-        &GeneratePlanOptions {
-            pin_pkgs: true,
-            ..Default::default()
-        },
-    )
-    .unwrap();
-    assert_plan_snapshot!(plan);
-}
+test_helper::generate_plan_tests!();
 
 #[test]
 fn test_custom_rust_version() {
@@ -267,36 +59,6 @@ fn test_rust_rocket_no_musl() {
     assert_plan_snapshot!(plan);
 }
 
-#[test]
-pub fn test_python() {
-    let plan = simple_gen_plan("./examples/python");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-pub fn test_python_poetry() {
-    let plan = simple_gen_plan("./examples/python-poetry");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_node_main_file() {
-    let plan = simple_gen_plan("./examples/node-main-file");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-pub fn test_python_setuptools() {
-    let plan = simple_gen_plan("./examples/python-setuptools");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_node_main_file_doesnt_exist() {
-    let plan = simple_gen_plan("./examples/node-main-file-not-exist");
-    assert_plan_snapshot!(plan);
-}
-
 #[test]
 fn test_haskell_stack() {
     let plan = simple_gen_plan("./examples/haskell-stack");
@@ -314,97 +76,6 @@ fn test_haskell_stack() {
     );
 }
 
-#[test]
-fn test_crystal() {
-    let plan = simple_gen_plan("./examples/crystal");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_overriding_environment_variables() {
-    let plan = generate_build_plan(
-        "./examples/node-variables",
-        vec!["NODE_ENV=test"],
-        &GeneratePlanOptions::default(),
-    )
-    .unwrap();
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_config_from_environment_variables() {
-    let plan = generate_build_plan(
-        "./examples/shell-hello",
-        vec![
-            "NIXPACKS_PKGS=cowsay ripgrep",
-            "NIXPACKS_INSTALL_CMD=install",
-            "NIXPACKS_BUILD_CMD=build",
-            "NIXPACKS_START_CMD=start",
-            "NIXPACKS_RUN_IMAGE=alpine",
-            "NIXPACKS_INSTALL_CACHE_DIRS=/tmp,foobar",
-            "NIXPACKS_BUILD_CACHE_DIRS=/build,barbaz",
-        ],
-        &GeneratePlanOptions::default(),
-    )
-    .unwrap();
-
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_staticfile() {
-    let plan = simple_gen_plan("./examples/staticfile");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_php_vanilla() {
-    let plan = simple_gen_plan("./examples/php-vanilla");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_php_laravel() {
-    let plan = simple_gen_plan("./examples/php-laravel");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_dart() {
-    let plan = simple_gen_plan("./examples/dart");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_swift() {
-    let plan = simple_gen_plan("./examples/swift");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_swift_vapor() {
-    let plan = simple_gen_plan("./examples/swift-vapor");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_java_maven() {
-    let plan = simple_gen_plan("./examples/java-maven");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_java_maven_wrapper() {
-    let plan = simple_gen_plan("./examples/java-maven-wrapper");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_zig() {
-    let plan = simple_gen_plan("./examples/zig");
-    assert_plan_snapshot!(plan);
-}
-
 #[cfg(any(target_arch = "aarch64", target_arch = "x86_64", target_arch = "i386"))]
 #[test]
 fn test_zig_gyro() {
@@ -427,27 +98,3 @@ fn test_zig_gyro() {
         .unwrap()
         .contains("mkdir /gyro"));
 }
-
-#[test]
-fn test_ruby_rails() {
-    let plan = simple_gen_plan("./examples/ruby-rails-postgres");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_ruby_sinatra() {
-    let plan = simple_gen_plan("./examples/ruby-sinatra");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_clojure() {
-    let plan = simple_gen_plan("./examples/clojure");
-    assert_plan_snapshot!(plan);
-}
-
-#[test]
-fn test_clojure_ring_app() {
-    let plan = simple_gen_plan("./examples/clojure-ring-app");
-    assert_plan_snapshot!(plan);
-}
diff --git a/tests/snapshots/generate_plan_tests__clojure_jdk11.snap b/tests/snapshots/generate_plan_tests__clojure_jdk11.snap
new file mode 100644
index 0000000..bca48e2
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__clojure_jdk11.snap
@@ -0,0 +1,38 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "leiningen"
+      },
+      {
+        "name": "jdk11"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      ""
+    ],
+    "cacheDirectories": [
+      "~/.m2"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "lein uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then  mv /app/target/uberjar/*standalone.jar /app/target/*standalone.jar; fi"
+    ]
+  },
+  "start": {
+    "cmd": "java $JAVA_OPTS -jar /app/target/*standalone.jar"
+  },
+  "variables": {},
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__clojure_jdk_latest.snap b/tests/snapshots/generate_plan_tests__clojure_jdk_latest.snap
new file mode 100644
index 0000000..7fd6b28
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__clojure_jdk_latest.snap
@@ -0,0 +1,38 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "leiningen"
+      },
+      {
+        "name": "jdk"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      ""
+    ],
+    "cacheDirectories": [
+      "~/.m2"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "lein uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then  mv /app/target/uberjar/*standalone.jar /app/target/*standalone.jar; fi"
+    ]
+  },
+  "start": {
+    "cmd": "java $JAVA_OPTS -jar /app/target/*standalone.jar"
+  },
+  "variables": {},
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__fsharp_cli.snap b/tests/snapshots/generate_plan_tests__fsharp_cli.snap
new file mode 100644
index 0000000..55e8538
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__fsharp_cli.snap
@@ -0,0 +1,36 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "dotnet-sdk"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      "dotnet restore"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "dotnet publish --no-restore -c Release -o out"
+    ]
+  },
+  "start": {
+    "cmd": "./out/fsharp-cli"
+  },
+  "variables": {
+    "ASPNETCORE_ENVIRONMENT": "Production",
+    "ASPNETCORE_URLS": "http://0.0.0.0:3000",
+    "DOTNET_ROOT": "/nix/var/nix/profiles/default/"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__go_gin.snap b/tests/snapshots/generate_plan_tests__go_gin.snap
new file mode 100644
index 0000000..d996419
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__go_gin.snap
@@ -0,0 +1,41 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "go_1_18"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      "go get"
+    ],
+    "cacheDirectories": [
+      "/root/.cache/go-build"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "go build -o out"
+    ],
+    "cacheDirectories": [
+      "/root/.cache/go-build"
+    ]
+  },
+  "start": {
+    "cmd": "./out",
+    "runImage": "debian:bullseye-slim"
+  },
+  "variables": {
+    "CGO_ENABLED": "0"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__haskell_stack.snap b/tests/snapshots/generate_plan_tests__haskell_stack.snap
new file mode 100644
index 0000000..ed0e8c4
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__haskell_stack.snap
@@ -0,0 +1,45 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "stack"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [
+      "libgmp-dev",
+      "gcc",
+      "binutils",
+      "make",
+      "zlib1g-dev"
+    ],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      "stack setup"
+    ],
+    "cacheDirectories": [
+      "/root/.stack"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "stack install"
+    ],
+    "cacheDirectories": [
+      "/root/.stack",
+      ".stack-work"
+    ]
+  },
+  "start": {
+    "cmd": "/root/.local/bin/haskell-stack-exe"
+  },
+  "variables": {},
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__node_bun.snap b/tests/snapshots/generate_plan_tests__node_bun.snap
new file mode 100644
index 0000000..b695d30
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node_bun.snap
@@ -0,0 +1,40 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "bun",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      "bun i --no-save"
+    ],
+    "cacheDirectories": [
+      "/root/.bun"
+    ]
+  },
+  "build": {
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "bun run start"
+  },
+  "variables": {
+    "CI": "true",
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__node_bun_no_start.snap b/tests/snapshots/generate_plan_tests__node_bun_no_start.snap
new file mode 100644
index 0000000..bec8b2d
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node_bun_no_start.snap
@@ -0,0 +1,40 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "bun",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      "bun i --no-save"
+    ],
+    "cacheDirectories": [
+      "/root/.bun"
+    ]
+  },
+  "build": {
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "bun index.ts"
+  },
+  "variables": {
+    "CI": "true",
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__node_bun_web_server.snap b/tests/snapshots/generate_plan_tests__node_bun_web_server.snap
new file mode 100644
index 0000000..bec8b2d
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node_bun_web_server.snap
@@ -0,0 +1,40 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "bun",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      "bun i --no-save"
+    ],
+    "cacheDirectories": [
+      "/root/.bun"
+    ]
+  },
+  "build": {
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "bun index.ts"
+  },
+  "variables": {
+    "CI": "true",
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__node_main_file_not_exist.snap b/tests/snapshots/generate_plan_tests__node_main_file_not_exist.snap
new file mode 100644
index 0000000..832cbeb
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node_main_file_not_exist.snap
@@ -0,0 +1,43 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs"
+      },
+      {
+        "name": "npm-8_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      "npm i"
+    ],
+    "cacheDirectories": [
+      "/root/.npm"
+    ]
+  },
+  "build": {
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "node index.js"
+  },
+  "variables": {
+    "CI": "true",
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__node_no_lockfile_canvas.snap b/tests/snapshots/generate_plan_tests__node_no_lockfile_canvas.snap
new file mode 100644
index 0000000..c1545f5
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node_no_lockfile_canvas.snap
@@ -0,0 +1,46 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs"
+      },
+      {
+        "name": "npm-8_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [
+      "libuuid",
+      "libGL"
+    ],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      "npm i"
+    ],
+    "cacheDirectories": [
+      "/root/.npm"
+    ]
+  },
+  "build": {
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "npm run start"
+  },
+  "variables": {
+    "CI": "true",
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__node_npm.snap b/tests/snapshots/generate_plan_tests__node_npm.snap
new file mode 100644
index 0000000..bb5bffc
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node_npm.snap
@@ -0,0 +1,46 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs"
+      },
+      {
+        "name": "npm-8_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      "npm ci"
+    ],
+    "cacheDirectories": [
+      "/root/.npm"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "npm run build"
+    ],
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "npm run start"
+  },
+  "variables": {
+    "CI": "true",
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__node_pnpm.snap b/tests/snapshots/generate_plan_tests__node_pnpm.snap
new file mode 100644
index 0000000..9a3c3ca
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node_pnpm.snap
@@ -0,0 +1,46 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs"
+      },
+      {
+        "name": "pnpm-6_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      "pnpm i --frozen-lockfile"
+    ],
+    "cacheDirectories": [
+      "/root/.cache/pnpm"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "pnpm run build"
+    ],
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "pnpm run start"
+  },
+  "variables": {
+    "CI": "true",
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__node_pnpm_custom_node_version.snap b/tests/snapshots/generate_plan_tests__node_pnpm_custom_node_version.snap
new file mode 100644
index 0000000..9a462e2
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node_pnpm_custom_node_version.snap
@@ -0,0 +1,46 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs-14_x"
+      },
+      {
+        "name": "pnpm-6_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      "pnpm i --frozen-lockfile"
+    ],
+    "cacheDirectories": [
+      "/root/.cache/pnpm"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "pnpm run build"
+    ],
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "pnpm run start"
+  },
+  "variables": {
+    "CI": "true",
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__node_pnpm_v7.snap b/tests/snapshots/generate_plan_tests__node_pnpm_v7.snap
new file mode 100644
index 0000000..402eea2
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node_pnpm_v7.snap
@@ -0,0 +1,46 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs"
+      },
+      {
+        "name": "pnpm-7_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      "pnpm i --frozen-lockfile"
+    ],
+    "cacheDirectories": [
+      "/root/.cache/pnpm"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "pnpm run build"
+    ],
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "pnpm run start"
+  },
+  "variables": {
+    "CI": "true",
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__node_variables.snap b/tests/snapshots/generate_plan_tests__node_variables.snap
new file mode 100644
index 0000000..832cbeb
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node_variables.snap
@@ -0,0 +1,43 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs"
+      },
+      {
+        "name": "npm-8_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      "npm i"
+    ],
+    "cacheDirectories": [
+      "/root/.npm"
+    ]
+  },
+  "build": {
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "node index.js"
+  },
+  "variables": {
+    "CI": "true",
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__node_yarn.snap b/tests/snapshots/generate_plan_tests__node_yarn.snap
new file mode 100644
index 0000000..eb89088
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node_yarn.snap
@@ -0,0 +1,46 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs"
+      },
+      {
+        "name": "yarn-1_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      "yarn install --frozen-lockfile"
+    ],
+    "cacheDirectories": [
+      "/usr/local/share/.cache/yarn/v6"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "yarn run build"
+    ],
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "yarn run start"
+  },
+  "variables": {
+    "CI": "true",
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__node_yarn_berry.snap b/tests/snapshots/generate_plan_tests__node_yarn_berry.snap
new file mode 100644
index 0000000..1abe7c2
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node_yarn_berry.snap
@@ -0,0 +1,46 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs-16_x"
+      },
+      {
+        "name": "yarn-1_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      "yarn set version berry && yarn install --check-cache"
+    ],
+    "cacheDirectories": [
+      "/usr/local/share/.cache/yarn/v6"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "yarn run build"
+    ],
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "yarn run start"
+  },
+  "variables": {
+    "CI": "true",
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__node_yarn_custom_node_version.snap b/tests/snapshots/generate_plan_tests__node_yarn_custom_node_version.snap
new file mode 100644
index 0000000..d10c36b
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node_yarn_custom_node_version.snap
@@ -0,0 +1,43 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs-14_x"
+      },
+      {
+        "name": "yarn-1_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      "yarn install --frozen-lockfile"
+    ],
+    "cacheDirectories": [
+      "/usr/local/share/.cache/yarn/v6"
+    ]
+  },
+  "build": {
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "yarn run start"
+  },
+  "variables": {
+    "CI": "true",
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__node_yarn_prisma.snap b/tests/snapshots/generate_plan_tests__node_yarn_prisma.snap
new file mode 100644
index 0000000..c1d4646
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node_yarn_prisma.snap
@@ -0,0 +1,46 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs-14_x"
+      },
+      {
+        "name": "yarn-1_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      "yarn install --frozen-lockfile"
+    ],
+    "cacheDirectories": [
+      "/usr/local/share/.cache/yarn/v6"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "yarn run build"
+    ],
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "yarn run start"
+  },
+  "variables": {
+    "CI": "true",
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__python_2.snap b/tests/snapshots/generate_plan_tests__python_2.snap
new file mode 100644
index 0000000..460e0b1
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__python_2.snap
@@ -0,0 +1,24 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "python27"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {},
+  "build": {},
+  "start": {
+    "cmd": "python main.py"
+  },
+  "variables": {},
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__python_2_runtime.snap b/tests/snapshots/generate_plan_tests__python_2_runtime.snap
new file mode 100644
index 0000000..460e0b1
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__python_2_runtime.snap
@@ -0,0 +1,24 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "python27"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {},
+  "build": {},
+  "start": {
+    "cmd": "python main.py"
+  },
+  "variables": {},
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__python_django.snap b/tests/snapshots/generate_plan_tests__python_django.snap
new file mode 100644
index 0000000..7d7c77b
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__python_django.snap
@@ -0,0 +1,43 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "python38"
+      },
+      {
+        "name": "postgresql"
+      },
+      {
+        "name": "gcc"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      "python -m venv /opt/venv && . /opt/venv/bin/activate && pip install -r requirements.txt"
+    ],
+    "onlyIncludeFiles": [
+      "requirements.txt"
+    ],
+    "cacheDirectories": [
+      "/root/.cache/pip"
+    ],
+    "paths": [
+      "/opt/venv/bin"
+    ]
+  },
+  "build": {},
+  "start": {
+    "cmd": "python manage.py migrate && gunicorn mysite.wsgi"
+  },
+  "variables": {},
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__python_numpy.snap b/tests/snapshots/generate_plan_tests__python_numpy.snap
new file mode 100644
index 0000000..6dff040
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__python_numpy.snap
@@ -0,0 +1,40 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "python38"
+      }
+    ],
+    "libraries": [
+      "zlib",
+      "stdenv.cc.cc.lib"
+    ],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      "python -m venv /opt/venv && . /opt/venv/bin/activate && pip install -r requirements.txt"
+    ],
+    "onlyIncludeFiles": [
+      "requirements.txt"
+    ],
+    "cacheDirectories": [
+      "/root/.cache/pip"
+    ],
+    "paths": [
+      "/opt/venv/bin"
+    ]
+  },
+  "build": {},
+  "start": {
+    "cmd": "python main.py"
+  },
+  "variables": {},
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__ruby.snap b/tests/snapshots/generate_plan_tests__ruby.snap
new file mode 100644
index 0000000..ed97e9d
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__ruby.snap
@@ -0,0 +1,48 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [],
+    "libraries": [],
+    "apt_pkgs": [
+      "procps"
+    ],
+    "cmds": [
+      "curl -sSL https://get.rvm.io | bash -s stable && . /etc/profile.d/rvm.sh",
+      "rvm install ruby-3.1.2",
+      "rvm --default use ruby-3.1.2",
+      "gem install bundler:2.3.7",
+      "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
+    ],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      "bundle install"
+    ],
+    "onlyIncludeFiles": [
+      "Gemfile*"
+    ],
+    "cacheDirectories": [
+      "/root/.bundle/cache"
+    ],
+    "paths": [
+      "/usr/local/rvm/rubies/ruby-3.1.2/bin",
+      "/usr/local/rvm/gems/ruby-3.1.2/bin",
+      "/usr/local/rvm/gems/ruby-3.1.2@global/bin"
+    ]
+  },
+  "build": {},
+  "start": {
+    "cmd": "ruby app.rb"
+  },
+  "variables": {
+    "BUNDLE_GEMFILE": "/app/Gemfile",
+    "GEM_HOME": "/usr/local/rvm/gems/ruby-3.1.2",
+    "GEM_PATH": "/usr/local/rvm/gems/ruby-3.1.2:/usr/local/rvm/gems/ruby-3.1.2@global"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__ruby_rails_postgres.snap b/tests/snapshots/generate_plan_tests__ruby_rails_postgres.snap
new file mode 100644
index 0000000..d771bcc
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__ruby_rails_postgres.snap
@@ -0,0 +1,53 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [],
+    "libraries": [],
+    "apt_pkgs": [
+      "procps",
+      "libpq-dev"
+    ],
+    "cmds": [
+      "curl -sSL https://get.rvm.io | bash -s stable && . /etc/profile.d/rvm.sh",
+      "rvm install 3.1.2",
+      "rvm --default use 3.1.2",
+      "gem install bundler:2.3.7",
+      "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
+    ],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      "bundle install"
+    ],
+    "onlyIncludeFiles": [
+      "Gemfile*"
+    ],
+    "cacheDirectories": [
+      "/root/.bundle/cache"
+    ],
+    "paths": [
+      "/usr/local/rvm/rubies/3.1.2/bin",
+      "/usr/local/rvm/gems/3.1.2/bin",
+      "/usr/local/rvm/gems/3.1.2@global/bin"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "bundle exec rake assets:precompile"
+    ]
+  },
+  "start": {
+    "cmd": "rake db:migrate && bundle exec bin/rails server -b 0.0.0.0 -p ${PORT:-3000}"
+  },
+  "variables": {
+    "BUNDLE_GEMFILE": "/app/Gemfile",
+    "GEM_HOME": "/usr/local/rvm/gems/3.1.2",
+    "GEM_PATH": "/usr/local/rvm/gems/3.1.2:/usr/local/rvm/gems/3.1.2@global"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__shell_hello.snap b/tests/snapshots/generate_plan_tests__shell_hello.snap
new file mode 100644
index 0000000..33c77e8
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__shell_hello.snap
@@ -0,0 +1,18 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {},
+  "build": {},
+  "start": {},
+  "variables": {},
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__swift_custom_version.snap b/tests/snapshots/generate_plan_tests__swift_custom_version.snap
new file mode 100644
index 0000000..dfc5d61
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__swift_custom_version.snap
@@ -0,0 +1,49 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "[version]",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "coreutils"
+      },
+      {
+        "name": "swift"
+      },
+      {
+        "name": "clang"
+      },
+      {
+        "name": "zlib"
+      },
+      {
+        "name": "zlib.dev"
+      }
+    ],
+    "archive": "[archive]",
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "[baseImage]"
+  },
+  "install": {
+    "cmds": [
+      "swift package resolve"
+    ],
+    "onlyIncludeFiles": [
+      "Package.swift"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "CC=clang++ swift build -c release --static-swift-stdlib",
+      "cp ./.build/release/swift ./swift && rm -rf ./.build"
+    ]
+  },
+  "start": {
+    "cmd": "./swift"
+  },
+  "variables": {},
+  "static_assets": {}
+}