Skip to content

Commit 76a8ca4

Browse files
authored
test: pin parameter, preset and tag values that flow from resources (#223)
Pins the parameter, preset, and tag values that flow from `resource` blocks, ahead of #221 (which evaluates only the parameter/preset/tag closure via coder/trivy#74). Everything passes on `main`, where nothing is pruned, so a failure after rebasing #221 means pruning dropped a resource a target depends on. - `testdata/resourceclosure`: the fixture from #221 plus conditional, `try()`, expression index, `option` and `dynamic "option"` blocks, `count` on a target, a data-source intermediary, tag -> resource -> parameter, preset `prebuilds`, and a local module. - `testdata/resourceclosurejson`: a `.tf.json` template with two resources in one expression. This currently fails against the #221 pin (second resource pruned, default unknown); see the [comment on coder/trivy#74](coder/trivy#74 (comment)). - `testdata/diskaccess`: revives #85. `file()` on an in-tree path is read; a path escaping the template root is unknown and flags the option as invalid. Not closure-related, but a cheap guard that resource pruning does not disturb function evaluation against the template fs. - `testdata/chain`: revives #164. Pins two-hop count chaining: a parameter count-gated on a count-gated parameter's `[0].value` is silently dropped today because its count is evaluated before the input flows in. The vector asserts current behavior and the comment says how to flip it once fixed. Prepared with Coder Agents assistance.
1 parent da5a06e commit 76a8ca4

11 files changed

Lines changed: 539 additions & 0 deletions

File tree

‎preview_test.go‎

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,132 @@ func Test_Extract(t *testing.T) {
5454
dir: "badparam",
5555
failPreview: true,
5656
},
57+
{
58+
// Parameters, presets, and tags whose values flow from resource
59+
// blocks. This pins the baseline so that any evaluation strategy
60+
// that skips resources (see coder/trivy#74) must keep every
61+
// reference shape here: direct, via local, count and for_each
62+
// index, transitive resource->resource, conditional, try(),
63+
// nested option and dynamic blocks, meta-arguments, data-source
64+
// intermediaries, and module inputs. An orphan resource nothing
65+
// references may be dropped without effect.
66+
name: "resource closure",
67+
dir: "resourceclosure",
68+
expTags: map[string]string{
69+
"flavor": "large", // tag reads a param-shared resource
70+
"tagged": "tag-large", // tag independently keeps its own resource
71+
"derived": "derived-large", // tag -> resource -> parameter
72+
},
73+
params: map[string]assertParam{
74+
"flavor": ap().value("large").def("large"), // via local
75+
"direct": ap().value("large").def("large"), // direct
76+
"indexed": ap().value("poolimg").def("poolimg"), // count index
77+
"byeach": ap().value("fe-large").def("fe-large"), // for_each index
78+
"chained": ap().value("chain-large").def("chain-large"), // transitive resource->resource
79+
80+
// Expression shapes the pruner must see through.
81+
"picked": ap().value("alt-large").def("alt-large"), // conditional
82+
"tried": ap().value("alt-large").def("alt-large"), // try()
83+
"varindexed": ap().value("poolimg").def("poolimg"), // index is an expression
84+
85+
// References inside nested blocks.
86+
"staticopt": ap().value("large").def("large").optVals("large"),
87+
"dynopts": ap().value("poolimg").def("poolimg").optVals("poolimg"),
88+
89+
// Meta-argument on the target reads a resource; the param only
90+
// exists if that resource survived.
91+
"gated": ap().value("on").def("on"),
92+
93+
// Non-target data source as the intermediary.
94+
"viadata": ap().value("large").def("large"),
95+
96+
// Module input carries a root resource in; module-owned resource
97+
// is never pruned.
98+
"viamodule": ap().value("large").def("large"),
99+
"modresource": ap().value("sub-large").def("sub-large"),
100+
},
101+
presets: map[string]assertPreset{
102+
// The preset independently keeps a resource nothing else references.
103+
"big": aPre().value("flavor", "preset-large"),
104+
// Nested prebuilds block reads a resource.
105+
"pre": aPre().value("flavor", "large").prebuildCount(2),
106+
},
107+
variables: map[string]assertVariable{
108+
"pick_alt": av().def(cty.BoolVal(true)).typeEq(cty.Bool),
109+
"idx": av().def(cty.NumberIntVal(1)).typeEq(cty.Number),
110+
},
111+
},
112+
{
113+
// JSON syntax templates. A single JSON expression that references two
114+
// resources must keep both; the parameter's default is the same as
115+
// for the HCL equivalent.
116+
name: "resource closure json",
117+
dir: "resourceclosurejson",
118+
expTags: map[string]string{},
119+
params: map[string]assertParam{
120+
"joined": ap().value("A-B").def("A-B"),
121+
},
122+
},
123+
{
124+
// file() resolves relative to the template root. A path inside the
125+
// template is read; a path that escapes it is unknown, which
126+
// invalidates that option but still renders the parameter.
127+
name: "diskaccess",
128+
dir: "diskaccess",
129+
expTags: map[string]string{},
130+
unknownTags: []string{},
131+
params: map[string]assertParam{
132+
"file": apWithDiags().
133+
value("hello world").def("hello world").
134+
optVals("hello world", types.UnknownStringValue).
135+
errorDiagnostics("Parameter contains 1 invalid options"),
136+
},
137+
},
138+
{
139+
// First hop of the chain: ide_selector is count-gated on git_repo,
140+
// which has no value yet, so only git_repo renders.
141+
name: "chain-no-inputs",
142+
dir: "chain",
143+
input: preview.Input{
144+
ParameterValues: map[string]string{},
145+
},
146+
expTags: map[string]string{},
147+
unknownTags: []string{},
148+
params: map[string]assertParam{
149+
"git_repo": apWithDiags().errorDiagnostics("Required"),
150+
},
151+
},
152+
{
153+
// Second hop of the chain. cpu_cores is count-gated on
154+
// data.coder_parameter.ide_selector[0].value, and ide_selector is
155+
// itself count-gated. With ide_selector supplied, local.selected
156+
// converges to ["GoLand"] (visible in Output.ModuleOutput), but
157+
// cpu_cores' count is evaluated before that input has flowed in
158+
// and count expansion is not revisited, so the parameter is
159+
// silently dropped. This pins that limitation.
160+
//
161+
// If chained count gating is fixed, add:
162+
//
163+
// "cpu_cores": ap().value("4"),
164+
//
165+
// (a number-typed parameter) and the parameter count assertion
166+
// will require all three.
167+
name: "chain-inputs",
168+
dir: "chain",
169+
input: preview.Input{
170+
ParameterValues: map[string]string{
171+
"git_repo": "coder/coder",
172+
"ide_selector": `["GoLand"]`,
173+
"cpu_cores": "4",
174+
},
175+
},
176+
expTags: map[string]string{},
177+
unknownTags: []string{},
178+
params: map[string]assertParam{
179+
"git_repo": ap().value("coder/coder"),
180+
"ide_selector": ap().value(`["GoLand"]`),
181+
},
182+
},
57183
{
58184
name: "sometags",
59185
dir: "sometags",

‎testdata/chain/main.tf‎

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
terraform {
2+
required_providers {
3+
coder = {
4+
source = "coder/coder"
5+
version = "2.5.3"
6+
}
7+
}
8+
}
9+
10+
// Two-hop count chaining: ide_selector exists only once git_repo has a value,
11+
// and cpu_cores exists only once ide_selector (itself count-gated, so reached
12+
// via [0]) has a non-empty selection. See the "chain" vectors in Test_Extract
13+
// for the current limitation on the second hop.
14+
locals {
15+
ides = [
16+
"VS Code",
17+
"JetBrains IntelliJ",
18+
"GoLand",
19+
"WebStorm",
20+
"PyCharm",
21+
"Databricks",
22+
"Jupyter Notebook",
23+
]
24+
25+
is_ml_repo = data.coder_parameter.git_repo.value == "coder/mlkit"
26+
27+
selected = try(jsondecode(data.coder_parameter.ide_selector[0].value), [])
28+
}
29+
30+
31+
data "coder_parameter" "git_repo" {
32+
name = "git_repo"
33+
display_name = "Git repo"
34+
description = "Select a git repo to work on."
35+
order = 1
36+
mutable = true
37+
type = "string"
38+
form_type = "dropdown"
39+
40+
option {
41+
# A Go-heavy repository
42+
name = "coder/coder"
43+
value = "coder/coder"
44+
}
45+
46+
option {
47+
# A python-heavy repository
48+
name = "coder/mlkit"
49+
value = "coder/mlkit"
50+
}
51+
}
52+
53+
data "coder_parameter" "ide_selector" {
54+
count = try(data.coder_parameter.git_repo.value, "") != "" ? 1 : 0
55+
name = "ide_selector"
56+
description = "Choose any IDEs for your workspace."
57+
mutable = true
58+
display_name = "Select mutliple IDEs"
59+
order = 1
60+
default = "[]"
61+
62+
# Allows users to select multiple IDEs from the list.
63+
form_type = "multi-select"
64+
type = "list(string)"
65+
66+
67+
dynamic "option" {
68+
for_each = local.ides
69+
content {
70+
name = option.value
71+
value = option.value
72+
}
73+
}
74+
}
75+
76+
77+
data "coder_parameter" "cpu_cores" {
78+
# Only show this parameter if the previous box is selected.
79+
count = length(local.selected) > 0 ? 1 : 0
80+
81+
name = "cpu_cores"
82+
display_name = "CPU Cores"
83+
type = "number"
84+
form_type = "slider"
85+
default = local.is_ml_repo ? 12 : 6
86+
order = 2
87+
validation {
88+
min = 1
89+
max = local.is_ml_repo ? 16 : 8
90+
}
91+
}
92+
93+
output "selected" {
94+
value = local.selected
95+
}
96+
97+
output "static" {
98+
value = "foo"
99+
}

‎testdata/chain/skipe2e‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
chained count gating is exercised by Test_Extract (static preview eval); real terraform apply is out of scope here

‎testdata/diskaccess/hello.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hello world

‎testdata/diskaccess/main.tf‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
terraform {
2+
required_providers {
3+
coder = {
4+
source = "coder/coder"
5+
version = "2.4.0-pre0"
6+
}
7+
}
8+
}
9+
10+
11+
data "coder_parameter" "file" {
12+
name = "file"
13+
description = "Attempt to read some files."
14+
type = "string"
15+
order = 1
16+
default = file("./hello.txt")
17+
18+
option {
19+
name = "Local"
20+
value = file("./hello.txt")
21+
}
22+
23+
option {
24+
name = "Outer"
25+
value = file("../README.md")
26+
}
27+
}

‎testdata/diskaccess/skipe2e‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
file() reads are exercised by Test_Extract (static preview eval); the outer path is intentionally unreadable

0 commit comments

Comments
 (0)