forked from apache/openwhisk
-
Notifications
You must be signed in to change notification settings - Fork 0
/
BasicActionRunnerTests.scala
330 lines (280 loc) · 10.9 KB
/
BasicActionRunnerTests.scala
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
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package actionContainers
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import spray.json.DefaultJsonProtocol._
import spray.json._
@RunWith(classOf[JUnitRunner])
trait BasicActionRunnerTests extends ActionProxyContainerTestUtils {
/**
* Must be defined by the runtime test suites. A typical implementation looks like this:
* withContainer("container-image-name", env)(code)
* See [[ActionContainer.withContainer]] for details.
*
* @param env the environment to pass to the container
* @param code the code to initialize with
*/
def withActionContainer(env: Map[String, String] = Map.empty)(code: ActionContainer => Unit): (String, String)
/**
* Runs tests for actions which receive an empty initializer (no source or exec).
*
* @param stub true if the proxy provides a stub
*/
def testNoSourceOrExec: TestConfig
/**
* Runs tests for actions which receive an empty code initializer (exec with code equal to the empty string).
*
* @param stub true if the proxy provides a stub
*/
def testNoSource = testNoSourceOrExec
/**
* Runs tests for actions which do not return a dictionary and confirms expected error messages.
*
* @param code code to execute, should not return a JSON object
* @param main the main function
* @param checkResultInLogs should be true iff the result of the action is expected to appear in stdout or stderr
*/
def testNotReturningJson: TestConfig
/**
* Tests that an action will not allow more than one initialization and confirms expected error messages.
*
* @param code the code to execute, the identity/echo function is sufficient.
* @param main the main function
*/
def testInitCannotBeCalledMoreThanOnce: TestConfig
/**
* Tests the echo action for an entry point other than "main".
*
* @param code the code to execute, must be the identity/echo function.
* @param main the main function to run, must not be "main"
*/
def testEntryPointOtherThanMain: TestConfig
/**
* Tests the echo action for different input parameters.
* The test actions must also print hello [stdout, stderr] to the respective streams.
*
* @param code the code to execute, must be the identity/echo function.
* @param main the main function
*/
def testEcho: TestConfig
/**
* Tests a unicode action. The action must properly handle unicode characters in the executable,
* receive a unicode character, and construct a response with a unicode character. It must also
* emit unicode characters correctly to stdout.
*
* @param code a function { delimiter } => { winter: "❄ " + delimiter + " ❄"}
* @param main the main function
*/
def testUnicode: TestConfig
/**
* Tests the action constructs the activation context correctly.
*
* @param code a function returning the activation context consisting of the following dictionary
* { "api_host": process.env.__OW_API_HOST,
* "api_key": process.env__OW_API_KEY,
* "namespace": process.env.__OW_NAMESPACE,
* "action_name": process.env.__OW_ACTION_NAME,
* "activation_id": process.env.__OW_ACTIVATION_ID,
* "deadline": process.env.__OW_DEADLINE
* }
* @param main the main function
* @param enforceEmptyOutputStream true to check empty stdout stream
* @param enforceEmptyErrorStream true to check empty stderr stream
*/
def testEnv: TestConfig
/**
* Tests the action to confirm it can handle a large parameter (larger than 128K) when using STDIN.
*
* @param code the identity/echo function.
* @param main the main function
*/
def testLargeInput: TestConfig
behavior of "runtime proxy"
it should "handle initialization with no code" in {
val config = testNoSource
val (out, err) = withActionContainer() { c =>
val (initCode, out) = c.init(initPayload("", ""))
if (config.hasCodeStub) {
initCode should be(200)
} else {
initCode should not be (200)
}
}
}
it should "handle initialization with no content" in {
val config = testNoSourceOrExec
val (out, err) = withActionContainer() { c =>
val (initCode, out) = c.init(JsObject.empty)
if (!config.hasCodeStub) {
initCode should not be (200)
out should {
be(Some(JsObject("error" -> JsString("Missing main/no code to execute.")))) or
be(Some(
JsObject("error" -> JsString("The action failed to generate or locate a binary. See logs for details."))))
}
} else {
initCode should be(200)
}
}
}
it should "run and report an error for function not returning a json object" in {
val config = testNotReturningJson
if (!config.skipTest) { // this may not be possible in types languages
val (out, err) = withActionContainer() { c =>
val (initCode, _) = c.init(initPayload(config.code, config.main))
initCode should be(200)
val (runCode, out) = c.run(JsObject.empty)
runCode should not be (200)
out should be(Some(JsObject("error" -> JsString("The action did not return a dictionary."))))
}
checkStreams(out, err, {
case (o, e) =>
// some runtimes may emita an error message,
// or for native runtimes emit the result to stdout
if (config.enforceEmptyOutputStream) o shouldBe empty
if (config.enforceEmptyErrorStream) e shouldBe empty
})
}
}
it should "fail to initialize a second time" in {
val config = testInitCannotBeCalledMoreThanOnce
val errorMessage = "Cannot initialize the action more than once."
val (out, err) = withActionContainer() { c =>
val (initCode1, _) = c.init(initPayload(config.code, config.main))
initCode1 should be(200)
val (initCode2, error2) = c.init(initPayload(config.code, config.main))
initCode2 should not be (200)
error2 should be(Some(JsObject("error" -> JsString(errorMessage))))
}
checkStreams(out, err, {
case (o, e) =>
(o + e) should include(errorMessage)
}, sentinelCount = 0)
}
it should s"invoke non-standard entry point" in {
val config = testEntryPointOtherThanMain
config.main should not be ("main")
val (out, err) = withActionContainer() { c =>
val (initCode, _) = c.init(initPayload(config.code, config.main))
initCode should be(200)
val arg = JsObject("string" -> JsString("hello"))
val (runCode, out) = c.run(runPayload(arg))
runCode should be(200)
out should be(Some(arg))
}
checkStreams(out, err, {
case (o, e) =>
// some native runtimes will emit the result to stdout
if (config.enforceEmptyOutputStream) o shouldBe empty
e shouldBe empty
})
}
it should s"echo arguments and print message to stdout/stderr" in {
val config = testEcho
val argss = List(
JsObject("string" -> JsString("hello")),
JsObject("string" -> JsString("❄ ☃ ❄")),
JsObject("numbers" -> JsArray(JsNumber(42), JsNumber(1))),
// JsObject("boolean" -> JsBoolean(true)), // fails with swift3 returning boolean: 1
JsObject("object" -> JsObject("a" -> JsString("A"))))
val (out, err) = withActionContainer() { c =>
val (initCode, _) = c.init(initPayload(config.code, config.main))
initCode should be(200)
for (args <- argss) {
val (runCode, out) = c.run(runPayload(args))
runCode should be(200)
out should be(Some(args))
}
}
checkStreams(out, err, {
case (o, e) =>
o should include("hello stdout")
// some languages may not support printing to stderr
if (!config.skipTest) e should include("hello stderr")
}, argss.length)
}
it should s"handle unicode in source, input params, logs, and result" in {
val config = testUnicode
val (out, err) = withActionContainer() { c =>
val (initCode, _) = c.init(initPayload(config.code, config.main))
initCode should be(200)
val (runCode, runRes) = c.run(runPayload(JsObject("delimiter" -> JsString("❄"))))
runRes.get.fields.get("winter") shouldBe Some(JsString("❄ ☃ ❄"))
}
checkStreams(out, err, {
case (o, _) =>
o.toLowerCase should include("❄ ☃ ❄")
})
}
it should s"confirm expected environment variables" in {
val config = testEnv
val props = Seq(
"api_host" -> "xyz",
"api_key" -> "abc",
"namespace" -> "zzz",
"action_name" -> "xxx",
"activation_id" -> "iii",
"deadline" -> "123")
val env = props.map { case (k, v) => s"__OW_${k.toUpperCase()}" -> v }
val (out, err) = withActionContainer(env.take(1).toMap) { c =>
val (initCode, _) = c.init(initPayload(config.code, config.main))
initCode should be(200)
val (runCode, out) = c.run(runPayload(JsObject.empty, Some(props.toMap.toJson.asJsObject)))
runCode should be(200)
out shouldBe defined
props.map {
case (k, v) =>
withClue(k) {
out.get.fields(k) shouldBe JsString(v)
}
}
}
checkStreams(out, err, {
case (o, e) =>
if (config.enforceEmptyOutputStream) o shouldBe empty
if (config.enforceEmptyErrorStream) e shouldBe empty
})
}
it should s"echo a large input" in {
val config = testLargeInput
if (!config.skipTest) {
var passed = true
val (out, err) = withActionContainer() { c =>
val (initCode, _) = c.init(initPayload(config.code, config.main))
initCode should be(200)
val arg = JsObject("arg" -> JsString(("a" * 1048561)))
val (_, runRes) = c.run(runPayload(arg))
if (runRes.get != arg) {
println(s"result did not match: ${runRes.get}")
passed = false
}
}
if (!passed) {
println(out)
println(err)
assert(false)
}
}
}
case class TestConfig(code: String,
main: String = "main",
enforceEmptyOutputStream: Boolean = true,
enforceEmptyErrorStream: Boolean = true,
hasCodeStub: Boolean = false,
skipTest: Boolean = false)
}