-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathScenarioTests.cs
302 lines (258 loc) · 11.1 KB
/
ScenarioTests.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
using Valleysoft.DockerfileModel.Tokens;
namespace Valleysoft.DockerfileModel.Tests;
public class ScenarioTests
{
/// <summary>
/// The structure of a Dockerfile consists of instructions, whitespace, comments, and parser directives.
/// </summary>
[Fact]
public void DockerfileStructureAndConstructTypes()
{
string dockerfileContent = TestHelper.ConcatLines(new List<string>
{
"# escape=`",
"FROM scratch",
" ",
"# TODO"
});
// Parse the Dockerfile
Dockerfile dockerfile = Dockerfile.Parse(dockerfileContent);
// Verify its structure
Assert.Equal('`', dockerfile.EscapeChar);
DockerfileConstruct[] constructs = dockerfile.Items.ToArray();
Assert.Equal(4, constructs.Length);
Assert.Equal(ConstructType.ParserDirective, constructs[0].Type);
Assert.IsType<ParserDirective>(constructs[0]);
Assert.Equal(ConstructType.Instruction, constructs[1].Type);
Assert.IsType<FromInstruction>(constructs[1]);
Assert.Equal(ConstructType.Whitespace, constructs[2].Type);
Assert.IsType<Whitespace>(constructs[2]);
Assert.Equal(ConstructType.Comment, constructs[3].Type);
Assert.IsType<Comment>(constructs[3]);
}
/// <summary>
/// Change the tag of an image name referenced in a FROM instruction.
/// </summary>
[Fact]
public void ChangeTag()
{
string dockerfileContent = TestHelper.ConcatLines(new List<string>
{
"FROM alpine:3.11",
"RUN echo \"Hello World\""
});
// Parse the Dockerfile
Dockerfile dockerfile = Dockerfile.Parse(dockerfileContent);
FromInstruction fromInstruction = dockerfile.Items.OfType<FromInstruction>().First();
// Parse the image name into its component parts
ImageName imageName = ImageName.Parse(fromInstruction.ImageName);
// Change the tag value and set the image name with the new value
imageName.Tag = "3.12";
fromInstruction.ImageName = imageName.ToString();
// Verify the new tag value is output
string expectedOutput = TestHelper.ConcatLines(new List<string>
{
"FROM alpine:3.12",
"RUN echo \"Hello World\""
});
Assert.Equal(expectedOutput, dockerfile.ToString());
}
/// <summary>
/// Resolves both overriden and default ARG values that are referenced throughout a Dockerfile.
/// </summary>
[Fact]
public void ResolveArguments_Globally()
{
string dockerfileContent = TestHelper.ConcatLines(new List<string>
{
"ARG REPO=alpine",
"ARG TAG=latest",
"FROM $REPO:$TAG"
});
// Parse the Dockerfile
Dockerfile dockerfile = Dockerfile.Parse(dockerfileContent);
// Resolve reference arg values, overriding TAG.
// This modifies the underlying values of the model, replacing any references to
// arguments with their resolved values. Be aware of this if your intention is
// write the model back to the Dockerfile on disk.
dockerfile.ResolveVariables(
new Dictionary<string, string>
{
{ "TAG", "3.12" }
},
options: new ResolutionOptions { UpdateInline = true });
// Verify the arg values have been resolved
string expectedOutput = TestHelper.ConcatLines(new List<string>
{
"ARG REPO=alpine",
"ARG TAG=latest",
"FROM alpine:3.12"
});
Assert.Equal(expectedOutput, dockerfile.ToString());
}
/// <summary>
/// Resolves ARG values that are referenced from an instruction without modifying the underlying model.
/// </summary>
[Fact]
public void ResolveArguments_FromValue()
{
string dockerfileContent = TestHelper.ConcatLines(new List<string>
{
"ARG REPO=alpine",
"ARG TAG=latest",
"FROM $REPO:$TAG"
});
// Parse the Dockerfile
Dockerfile dockerfile = Dockerfile.Parse(dockerfileContent);
FromInstruction fromInstruction = dockerfile.Items.OfType<FromInstruction>().First();
// Resolve arg values on the specifically on the FROM instruction and have the resolved value returned
// without modifying the underlying model.
string resolvedImageName = dockerfile.ResolveVariables(fromInstruction);
// Verify the image name has the args resolved
Assert.Equal("FROM alpine:latest", resolvedImageName);
// Verify the underlying value has the arg references maintained
string expectedOutput = TestHelper.ConcatLines(new List<string>
{
"ARG REPO=alpine",
"ARG TAG=latest",
"FROM $REPO:$TAG"
});
Assert.Equal(expectedOutput, dockerfile.ToString());
}
/// <summary>
/// Each construct within a Dockerfile is made up of tokens of various types. Some tokens
/// are aggregate tokens that contain other, more primitive, tokens.
/// </summary>
[Fact]
public void Tokens()
{
string dockerfileContent = TestHelper.ConcatLines(new List<string>
{
"ARG TAG=latest",
"FROM alpine:$TAG \\",
" AS build"
});
// Parse the Dockerfile
Dockerfile dockerfile = Dockerfile.Parse(dockerfileContent);
DockerfileConstruct[] dockerfileConstructs = dockerfile.Items.ToArray();
// Verify the individual tokens that are contained in the ARG instruction
Token[] repoArgTokens = dockerfileConstructs[0].Tokens.ToArray();
Assert.Equal(4, repoArgTokens.Length);
Assert.IsType<KeywordToken>(repoArgTokens[0]);
Assert.IsType<WhitespaceToken>(repoArgTokens[1]);
Assert.IsType<ArgDeclaration>(repoArgTokens[2]);
Assert.IsType<NewLineToken>(repoArgTokens[3]);
// Verify the individual tokens that are contained in the FROM instruction
Token[] fromInstructionTokens = dockerfileConstructs[1].Tokens.ToArray();
Assert.Equal(9, fromInstructionTokens.Length);
Assert.IsType<KeywordToken>(fromInstructionTokens[0]);
Assert.IsType<WhitespaceToken>(fromInstructionTokens[1]);
Assert.IsType<LiteralToken>(fromInstructionTokens[2]);
Assert.IsType<WhitespaceToken>(fromInstructionTokens[3]);
// LineContinuation is an aggregate token that contains other tokens
Assert.IsType<LineContinuationToken>(fromInstructionTokens[4]);
LineContinuationToken lineContinuation = (LineContinuationToken)fromInstructionTokens[4];
Token[] lineContinuationTokens = lineContinuation.Tokens.ToArray();
Assert.Equal(2, lineContinuationTokens.Length);
Assert.IsType<SymbolToken>(lineContinuationTokens[0]);
Assert.IsType<NewLineToken>(lineContinuationTokens[1]);
Assert.IsType<WhitespaceToken>(fromInstructionTokens[5]);
Assert.IsType<KeywordToken>(fromInstructionTokens[6]);
Assert.IsType<WhitespaceToken>(fromInstructionTokens[7]);
Assert.IsType<StageName>(fromInstructionTokens[8]);
}
/// <summary>
/// Comments can either be top-level or nested within a multi-line instruction.
/// </summary>
[Fact]
public void Comments()
{
string dockerfileContent = TestHelper.ConcatLines(new List<string>
{
"# top-level comment",
"FROM alpine \\",
" # nested comment",
" AS build",
"# top-level comment",
});
// Parse the Dockerfile
Dockerfile dockerfile = Dockerfile.Parse(dockerfileContent);
DockerfileConstruct[] dockerfileConstructs = dockerfile.Items.ToArray();
Assert.Equal(3, dockerfileConstructs.Length);
Assert.IsType<Comment>(dockerfileConstructs[0]);
// The FROM instruction contains a comment within its context. Any comments that
// are interspersed amongst the lines of an instruction that spans multiple lines
// will be contained as comments within the instruction, rather than as top-level
// comments of the Dockerfile.
Assert.IsType<FromInstruction>(dockerfileConstructs[1]);
FromInstruction fromInstruction = (FromInstruction)dockerfileConstructs[1];
Assert.Single(fromInstruction.Comments);
Assert.IsType<Comment>(dockerfileConstructs[2]);
}
/// <summary>
/// Create a Dockerfile from scratch using the fluent API of DockerfileBuilder.
/// </summary>
[Fact]
public void CreateNewDockerfileWithDockerfileBuilder()
{
DockerfileBuilder builder = new();
builder
.Comment("Made from scratch Dockerfile")
.NewLine()
.ArgInstruction("TAG", "latest")
.FromInstruction("alpine:$TAG")
.ArgInstruction("MESSAGE")
.RunInstruction("echo $MESSAGE");
string expectedOutput =
"# Made from scratch Dockerfile" + Environment.NewLine +
Environment.NewLine +
"ARG TAG=latest" + Environment.NewLine +
"FROM alpine:$TAG" + Environment.NewLine +
"ARG MESSAGE" + Environment.NewLine +
"RUN echo $MESSAGE" + Environment.NewLine;
Assert.Equal(expectedOutput, builder.Dockerfile.ToString());
}
/// <summary>
/// Create a fully-customized Dockerfile from scratch using the fluent API of TokenBuilder.
/// </summary>
[Fact]
public void CreateNewDockerfileWithTokenBuilder()
{
DockerfileBuilder builder = new();
builder
.Comment("Made from scratch Dockerfile")
.NewLine()
.ArgInstruction("TAG", "latest")
.FromInstruction("alpine:$TAG")
.ArgInstruction("MESSAGE")
.RunInstruction(tokenBuilder =>
{
// Configure the RUN instruction to have a line continuation with an inline comment
tokenBuilder
.Keyword("RUN")
.Whitespace(" ")
.LineContinuation()
.Whitespace(" ")
.Comment(" Output message")
.NewLine()
.Whitespace(" ")
.ShellFormCommand(tokenBuilder =>
{
tokenBuilder.Literal(tokenBuilder =>
tokenBuilder
.String("echo ")
.VariableRef("MESSAGE"));
});
});
string expectedOutput =
"# Made from scratch Dockerfile" + Environment.NewLine +
Environment.NewLine +
"ARG TAG=latest" + Environment.NewLine +
"FROM alpine:$TAG" + Environment.NewLine +
"ARG MESSAGE" + Environment.NewLine +
"RUN \\" + Environment.NewLine +
" # Output message" + Environment.NewLine +
" echo $MESSAGE" + Environment.NewLine;
Assert.Equal(expectedOutput, builder.Dockerfile.ToString());
}
}