core/generate/context_test.go
core/generate/context_test.goBrowse 1970 files
3,566 tokens
13,031 bytes
Token encoding: o200k_base
Snapshot 21a254f
← Back to SKILL.md
1package generate2 3import (4 "encoding/json"5 "os"6 "path/filepath"7 "slices"8 "testing"9 10 "github.com/gkampitakis/go-snaps/snaps"11 "github.com/railwayapp/railpack/core/app"12 "github.com/railwayapp/railpack/core/config"13 "github.com/railwayapp/railpack/core/logger"14 "github.com/railwayapp/railpack/core/plan"15 "github.com/stretchr/testify/require"16)17 18type TestProvider struct{}19 20func (p *TestProvider) Plan(ctx *GenerateContext) error {21 // mise22 mise := ctx.GetMiseStepBuilder()23 nodeRef := mise.Default("node", "18")24 mise.Version(nodeRef, "18", "test")25 26 // commands27 installStep := ctx.NewCommandStep("install")28 installStep.AddCommand(plan.NewExecCommand("npm install", plan.ExecOptions{}))29 installStep.AddInput(mise.GetLayer())30 installStep.Secrets = []string{}31 32 buildStep := ctx.NewCommandStep("build")33 buildStep.AddCommand(plan.NewExecCommand("npm run build", plan.ExecOptions{}))34 buildStep.AddInput(plan.NewStepLayer(installStep.Name()))35 36 ctx.Deploy.DeployInputs = []plan.Layer{37 plan.NewStepLayer(buildStep.Name()),38 }39 40 return nil41}42 43func CreateTestContext(t *testing.T, path string) *GenerateContext {44 t.Helper()45 46 userApp, err := app.NewApp(path)47 require.NoError(t, err)48 49 env := app.NewEnvironment(nil)50 config := config.EmptyConfig()51 52 ctx, err := NewGenerateContext(userApp, env, config, logger.NewLogger())53 require.NoError(t, err)54 55 return ctx56}57 58func TestGenerateContext(t *testing.T) {59 ctx := CreateTestContext(t, "../../examples/node-npm")60 provider := &TestProvider{}61 require.NoError(t, provider.Plan(ctx))62 63 // User defined config64 configJSON := `{65 "packages": {66 "node": "20.18.2",67 "go": "1.23.5",68 "python": "3.13.1"69 },70 "aptPackages": ["curl"],71 "steps": {72 "build": {73 "commands": ["echo building"]74 }75 },76 "secrets": ["RAILWAY_SECRET_1", "RAILWAY_SECRET_2"],77 "deploy": {78 "startCommand": "echo hello",79 "variables": {80 "HELLO": "world"81 }82 }83 }`84 85 var config config.Config86 require.NoError(t, json.Unmarshal([]byte(configJSON), &config))87 88 ctx.Config = &config89 90 buildPlan, _, err := ctx.Generate()91 require.NoError(t, err)92 93 buildPlanJSON, err := json.MarshalIndent(buildPlan, "", " ")94 require.NoError(t, err)95 96 var actualPlan map[string]any97 require.NoError(t, json.Unmarshal(buildPlanJSON, &actualPlan))98 99 serializedPlan, err := json.MarshalIndent(actualPlan, "", " ")100 require.NoError(t, err)101 102 snaps.MatchJSON(t, serializedPlan)103}104 105func TestGenerateContextAppliesConfiguredAptPackages(t *testing.T) {106 t.Run("deprecated build packages remain additive", func(t *testing.T) {107 ctx := CreateTestContext(t, "../../examples/node-npm")108 ctx.GetMiseStepBuilder().AddSupportingAptPackage("gcc")109 110 cfg := config.EmptyConfig()111 cfg.BuildAptPackages = []string{"curl"}112 ctx.Config = cfg113 114 ctx.applyConfig()115 116 require.Equal(t, []string{"gcc", "curl"}, ctx.GetMiseStepBuilder().SupportingAptPackages)117 require.Equal(t, logger.Deprecation, ctx.Logger.Logs[0].Level)118 require.Contains(t, ctx.Logger.Logs[0].Msg, "in the future")119 require.Equal(t, logger.Suggestion, ctx.Logger.Logs[1].Level)120 require.Contains(t, ctx.Logger.Logs[1].Msg, "Add `...` to `buildAptPackages`")121 require.Equal(t, "/guides/installing-packages", ctx.Logger.Logs[1].DocsPath)122 })123 124 t.Run("build packages explicitly extend generated packages", func(t *testing.T) {125 ctx := CreateTestContext(t, "../../examples/node-npm")126 ctx.GetMiseStepBuilder().AddSupportingAptPackage("gcc")127 128 cfg := config.EmptyConfig()129 cfg.BuildAptPackages = []string{"...", "curl"}130 ctx.Config = cfg131 132 ctx.applyConfig()133 134 require.Equal(t, []string{"gcc", "curl"}, ctx.GetMiseStepBuilder().SupportingAptPackages)135 })136 137 t.Run("deploy packages replace generated packages", func(t *testing.T) {138 ctx := CreateTestContext(t, "../../examples/node-npm")139 ctx.Deploy.AddAptPackages([]string{"libnss3"})140 141 cfg := config.EmptyConfig()142 cfg.Deploy.AptPackages = []string{"curl"}143 ctx.Config = cfg144 145 ctx.applyConfig()146 147 require.Equal(t, []string{"curl"}, ctx.Deploy.AptPackages)148 require.Equal(t, logger.Suggestion, ctx.Logger.Logs[0].Level)149 require.Contains(t, ctx.Logger.Logs[0].Msg, "Add `...` to `deploy.aptPackages`")150 require.Equal(t, "/guides/installing-packages", ctx.Logger.Logs[0].DocsPath)151 })152 153 t.Run("deploy packages explicitly extend generated packages", func(t *testing.T) {154 ctx := CreateTestContext(t, "../../examples/node-npm")155 ctx.Deploy.AddAptPackages([]string{"libnss3"})156 157 cfg := config.EmptyConfig()158 cfg.Deploy.AptPackages = []string{"...", "curl"}159 ctx.Config = cfg160 161 ctx.applyConfig()162 163 require.Equal(t, []string{"libnss3", "curl"}, ctx.Deploy.AptPackages)164 })165}166 167func TestGenerateContextAppliesConfiguredDeployBase(t *testing.T) {168 t.Run("direct deploy base", func(t *testing.T) {169 ctx := CreateTestContext(t, "../../examples/node-npm")170 cfg := config.EmptyConfig()171 cfg.Deploy.Base = &plan.Layer{Image: "debian:trixie-slim"}172 ctx.Config = cfg173 174 buildPlan, _, err := ctx.Generate()175 require.NoError(t, err)176 require.Equal(t, plan.NewImageLayer("debian:trixie-slim"), buildPlan.Deploy.Base)177 })178 179 t.Run("runtime apt step uses configured deploy base", func(t *testing.T) {180 ctx := CreateTestContext(t, "../../examples/node-npm")181 cfg := config.EmptyConfig()182 cfg.Deploy.Base = &plan.Layer{Image: "debian:trixie-slim"}183 cfg.Deploy.AptPackages = []string{"curl"}184 ctx.Config = cfg185 186 buildPlan, _, err := ctx.Generate()187 require.NoError(t, err)188 require.Equal(t, plan.NewStepLayer("packages:apt:runtime"), buildPlan.Deploy.Base)189 190 var runtimeAptStep *plan.Step191 for i := range buildPlan.Steps {192 if buildPlan.Steps[i].Name == "packages:apt:runtime" {193 runtimeAptStep = &buildPlan.Steps[i]194 break195 }196 }197 198 require.NotNil(t, runtimeAptStep)199 require.Equal(t, []plan.Layer{plan.NewImageLayer("debian:trixie-slim")}, runtimeAptStep.Inputs)200 })201}202 203func TestGenerateContextDeployInputs(t *testing.T) {204 t.Run("explicit inputs suppress implicit outputs from every configured step", func(t *testing.T) {205 ctx := CreateTestContext(t, "../../examples/node-npm")206 provider := &TestProvider{}207 require.NoError(t, provider.Plan(ctx))208 209 configJSON := `{210 "steps": {211 "install": {212 "commands": ["echo installing"]213 },214 "build": {215 "commands": ["echo building"]216 }217 },218 "deploy": {219 "inputs": [220 {221 "step": "build",222 "include": ["apps/landing/.next/standalone"]223 }224 ]225 }226 }`227 228 var config config.Config229 require.NoError(t, json.Unmarshal([]byte(configJSON), &config))230 ctx.Config = &config231 232 buildPlan, _, err := ctx.Generate()233 require.NoError(t, err)234 235 require.Equal(t, []plan.Layer{236 plan.NewStepLayer("build", plan.NewIncludeFilter([]string{"apps/landing/.next/standalone"})),237 }, buildPlan.Deploy.Inputs)238 })239 240 t.Run("omitted inputs preserve implicit outputs", func(t *testing.T) {241 ctx := CreateTestContext(t, "../../examples/node-npm")242 configJSON := `{243 "steps": {244 "custom": {245 "commands": ["echo custom"]246 }247 },248 "deploy": {249 "startCommand": "echo hello"250 }251 }`252 253 var config config.Config254 require.NoError(t, json.Unmarshal([]byte(configJSON), &config))255 ctx.Config = &config256 257 buildPlan, _, err := ctx.Generate()258 require.NoError(t, err)259 260 require.Equal(t, []plan.Layer{261 plan.NewStepLayer("custom", plan.NewIncludeFilter([]string{"."})),262 }, buildPlan.Deploy.Inputs)263 })264 265 t.Run("explicit deploy outputs remain additive", func(t *testing.T) {266 ctx := CreateTestContext(t, "../../examples/node-npm")267 configJSON := `{268 "steps": {269 "build": {270 "commands": ["echo building"],271 "deployOutputs": [272 {"include": ["dist"]}273 ]274 }275 },276 "deploy": {277 "inputs": [278 {"step": "build", "include": ["other"]}279 ]280 }281 }`282 283 var config config.Config284 require.NoError(t, json.Unmarshal([]byte(configJSON), &config))285 ctx.Config = &config286 287 buildPlan, _, err := ctx.Generate()288 require.NoError(t, err)289 290 require.Equal(t, []plan.Layer{291 plan.NewStepLayer("build", plan.NewIncludeFilter([]string{"other"})),292 plan.NewStepLayer("build", plan.NewIncludeFilter([]string{"dist"})),293 }, buildPlan.Deploy.Inputs)294 })295 296 t.Run("empty inputs suppress implicit outputs", func(t *testing.T) {297 ctx := CreateTestContext(t, "../../examples/node-npm")298 provider := &TestProvider{}299 require.NoError(t, provider.Plan(ctx))300 301 configJSON := `{302 "steps": {303 "install": {304 "commands": ["echo installing"]305 },306 "build": {307 "commands": ["echo building"]308 }309 },310 "deploy": {311 "inputs": []312 }313 }`314 315 var config config.Config316 require.NoError(t, json.Unmarshal([]byte(configJSON), &config))317 ctx.Config = &config318 319 buildPlan, _, err := ctx.Generate()320 require.NoError(t, err)321 322 require.Empty(t, buildPlan.Deploy.Inputs)323 })324 325 t.Run("spread inputs preserve generated and implicit outputs", func(t *testing.T) {326 ctx := CreateTestContext(t, "../../examples/node-npm")327 provider := &TestProvider{}328 require.NoError(t, provider.Plan(ctx))329 330 configJSON := `{331 "steps": {332 "custom": {333 "commands": ["echo custom"]334 }335 },336 "deploy": {337 "inputs": ["..."]338 }339 }`340 341 var config config.Config342 require.NoError(t, json.Unmarshal([]byte(configJSON), &config))343 ctx.Config = &config344 345 buildPlan, _, err := ctx.Generate()346 require.NoError(t, err)347 348 require.Equal(t, []plan.Layer{349 plan.NewStepLayer("build"),350 plan.NewStepLayer("custom", plan.NewIncludeFilter([]string{"."})),351 }, buildPlan.Deploy.Inputs)352 })353}354 355func TestGenerateContextDockerignore(t *testing.T) {356 t.Run("dockerignore patterns precede config patterns", func(t *testing.T) {357 ctx := CreateTestContext(t, "../../examples/dockerignore")358 configExcludes := []string{"!the.log", "config-only"}359 ctx.Config.Exclude = configExcludes360 361 buildPlan, _, err := ctx.Generate()362 require.NoError(t, err)363 364 expected := append(slices.Clone(ctx.dockerignoreCtx.Excludes), configExcludes...)365 require.Equal(t, expected, buildPlan.Exclude)366 })367 368 t.Run("context with dockerignore", func(t *testing.T) {369 ctx := CreateTestContext(t, "../../examples/dockerignore")370 371 // Verify dockerignore was parsed during context creation372 require.NotNil(t, ctx.dockerignoreCtx)373 374 // Verify metadata indicates dockerignore presence375 require.Equal(t, "true", ctx.Metadata.Get("dockerIgnore"))376 377 // Verify dockerignore patterns are in the context378 require.NotEmpty(t, ctx.dockerignoreCtx.Excludes)379 require.Contains(t, ctx.dockerignoreCtx.Excludes, ".vscode")380 require.Contains(t, ctx.dockerignoreCtx.Excludes, "*.log")381 require.Contains(t, ctx.dockerignoreCtx.Excludes, "__pycache__")382 383 // Negation patterns are also in Excludes (with ! prefix)384 require.Contains(t, ctx.dockerignoreCtx.Excludes, "!negation_test/should_exist.txt")385 require.Contains(t, ctx.dockerignoreCtx.Excludes, "!negation_test/existing_folder")386 387 // Verify dockerignore patterns are correctly moved to the plan level388 buildPlan, _, err := ctx.Generate()389 require.NoError(t, err)390 require.Contains(t, buildPlan.Exclude, ".vscode")391 require.Contains(t, buildPlan.Exclude, "*.log")392 require.Contains(t, buildPlan.Exclude, "!negation_test/should_exist.txt")393 require.Contains(t, buildPlan.Exclude, "!negation_test/existing_folder")394 })395 396 t.Run("context without dockerignore", func(t *testing.T) {397 ctx := CreateTestContext(t, "../../examples/node-npm")398 399 // Verify dockerignore context exists but has no patterns400 require.NotNil(t, ctx.dockerignoreCtx)401 402 // Verify metadata does not indicate dockerignore presence403 require.Empty(t, ctx.Metadata.Get("dockerIgnore"))404 })405 406 t.Run("context creation with no dockerignore", func(t *testing.T) {407 // Test with a directory that exists but has no .dockerignore file408 ctx := CreateTestContext(t, "../../examples/node-npm")409 410 // Should succeed even without .dockerignore file411 require.NotNil(t, ctx)412 require.NotNil(t, ctx.dockerignoreCtx)413 414 // Verify parsing works with no file present415 require.Nil(t, ctx.dockerignoreCtx.Excludes)416 })417 418 t.Run("context creation fails with invalid dockerignore", func(t *testing.T) {419 // Create a temporary directory with an inaccessible .dockerignore420 tempDir, err := os.MkdirTemp("", "dockerignore-test")421 require.NoError(t, err)422 defer func() { _ = os.RemoveAll(tempDir) }()423 424 dockerignorePath := filepath.Join(tempDir, ".dockerignore")425 err = os.WriteFile(dockerignorePath, []byte("*.log\nnode_modules\n"), 0644)426 require.NoError(t, err)427 428 // Make the file unreadable to simulate a parsing error429 err = os.Chmod(dockerignorePath, 0000)430 require.NoError(t, err)431 defer func() { _ = os.Chmod(dockerignorePath, 0644) }()432 433 // Create app with the temp directory434 userApp, err := app.NewApp(tempDir)435 require.NoError(t, err)436 437 env := app.NewEnvironment(nil)438 config := config.EmptyConfig()439 440 // Context creation should fail due to dockerignore parsing error441 ctx, err := NewGenerateContext(userApp, env, config, logger.NewLogger())442 require.Error(t, err)443 require.Contains(t, err.Error(), "failed to parse .dockerignore")444 require.Nil(t, ctx)445 })446}447