blob: c94aecb384c6ebc0dad7c489d800754f18df0f09 [file] [log] [blame]
Devin Lime1346f42018-05-15 15:41:36 -07001#!groovy
Jeremy Ronquillo6fbfdd52019-07-09 13:49:34 -07002// Copyright 2019 Open Networking Foundation (ONF)
Devin Limf5175192018-05-14 19:13:22 -07003//
4// Please refer questions to either the onos test mailing list at <onos-test@onosproject.org>,
5// the System Testing Plans and Results wiki page at <https://wiki.onosproject.org/x/voMg>,
6// or the System Testing Guide page at <https://wiki.onosproject.org/x/WYQg>
7//
8// TestON is free software: you can redistribute it and/or modify
9// it under the terms of the GNU General Public License as published by
10// the Free Software Foundation, either version 2 of the License, or
11// (at your option) any later version.
12//
13// TestON is distributed in the hope that it will be useful,
14// but WITHOUT ANY WARRANTY; without even the implied warranty of
15// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16// GNU General Public License for more details.
17//
18// You should have received a copy of the GNU General Public License
19// along with TestON. If not, see <http://www.gnu.org/licenses/>.
20
Jeremy Ronquillo6fbfdd52019-07-09 13:49:34 -070021// This is the Jenkins script for master-trigger
Devin Limf5175192018-05-14 19:13:22 -070022
Jeremy Ronquillo336110a2019-07-11 14:20:40 -070023import groovy.time.TimeCategory
24import groovy.time.TimeDuration
25
Devin Limf5175192018-05-14 19:13:22 -070026// set the functions of the dependencies.
Jeremy Ronquillo336110a2019-07-11 14:20:40 -070027graphs = evaluate readTrusted( 'TestON/JenkinsFile/dependencies/JenkinsGraphs.groovy' )
Devin Limb734ea52018-05-14 14:13:05 -070028fileRelated = evaluate readTrusted( 'TestON/JenkinsFile/dependencies/JenkinsPathAndFiles.groovy' )
Jeremy Ronquillo64eeeb12019-05-13 11:19:46 -070029test_list = evaluate readTrusted( 'TestON/JenkinsFile/dependencies/JenkinsTestONTests.groovy' )
Devin Lim2edfcec2018-05-09 17:16:21 -070030
Jeremy Ronquillo6da78cf2019-07-29 11:47:19 -070031INITIALIZATION_TIMEOUT_MINUTES = 10 // timeout init() function if it takes too long.
32
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -070033onos_tag = null
34manually_run = null
35now = null
36today = null
37onos_branches = null
38day = null
39post_result = null
40branchesParam = null
41isFabric = null
42testsParam = null
Jeremy Ronquillo3c2f10d2019-06-10 16:54:46 -070043simulateDay = null
Jeremy Ronquillo6da78cf2019-07-29 11:47:19 -070044pipelineTimeOut = null
Jeremy Ronquillo4fd82442019-05-21 20:56:58 -070045
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -070046dayMap = [:]
47fullDayMap = [:]
Jeremy Ronquilloa37920b2019-05-23 14:34:25 -070048all_testcases = [:]
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -070049runTest = [:]
50selectedTests = [:]
51graphPaths = [:]
Devin Lim2edfcec2018-05-09 17:16:21 -070052
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -070053main()
Devin Limf5175192018-05-14 19:13:22 -070054
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -070055def main() {
Jeremy Ronquillo6da78cf2019-07-29 11:47:19 -070056 timeout( time: INITIALIZATION_TIMEOUT_MINUTES, unit: "MINUTES" ){
57 init()
58 }
59 timeout( time: pipelineTimeOut, unit: "MINUTES" ){
60 runTests()
61 generateGraphs()
62 }
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -070063}
64
65// **************
66// Initialization
67// **************
68
69// initialize file scope vars
70def init(){
71 // get the name of the job.
72 jobName = env.JOB_NAME
73
74 // set the versions of the onos
75 fileRelated.init()
76 test_list.init()
77 readParams()
78
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -070079 // list of the tests to be run will be saved in each choices.
80 day = ""
81
82 initDates()
83 onos_branches = getONOSBranches()
84 selectedTests = getONOSTests()
Jeremy Ronquilloa5aa7c12019-06-04 10:26:36 -070085
Jeremy Ronquillod98c2a12019-06-07 15:13:29 -070086 initGraphPaths()
87
Jeremy Ronquilloa5aa7c12019-06-04 10:26:36 -070088 echo "selectedTests: " + selectedTests
Jeremy Ronquillo3c2f10d2019-06-10 16:54:46 -070089 echo "onos_branches: " + onos_branches
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -070090}
91
92def readParams(){
93 // get post result from the params for manually run.
94 post_result = params.PostResult
95 manually_run = params.manual_run
96 onos_tag = params.ONOSTag
97 branchesParam = params.branches
Jeremy Ronquillo336110a2019-07-11 14:20:40 -070098 isOldFlow = true // hardcoding to true since we are always using oldFlow.
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -070099 testsParam = params.Tests
100 isFabric = params.isFabric
Jeremy Ronquillo3c2f10d2019-06-10 16:54:46 -0700101 simulateDay = params.simulate_day
Jeremy Ronquillo6da78cf2019-07-29 11:47:19 -0700102 pipelineTimeOut = params.TimeOut.toInteger()
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700103}
Devin Lim2edfcec2018-05-09 17:16:21 -0700104
105// Set tests based on day of week
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700106def initDates(){
107 echo "-> initDates()"
Jeremy Ronquillo336110a2019-07-11 14:20:40 -0700108 now = getCurrentTime()
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700109 dayMap = [ ( Calendar.MONDAY ) : "mon",
110 ( Calendar.TUESDAY ) : "tue",
111 ( Calendar.WEDNESDAY ) : "wed",
112 ( Calendar.THURSDAY ) : "thu",
113 ( Calendar.FRIDAY ) : "fri",
114 ( Calendar.SATURDAY ) : "sat",
115 ( Calendar.SUNDAY ) : "sun" ]
116 fullDayMap = [ ( Calendar.MONDAY ) : "Monday",
117 ( Calendar.TUESDAY ) : "Tuesday",
118 ( Calendar.WEDNESDAY ) : "Wednesday",
119 ( Calendar.THURSDAY ) : "Thursday",
120 ( Calendar.FRIDAY ) : "Friday",
121 ( Calendar.SATURDAY ) : "Saturday",
122 ( Calendar.SUNDAY ) : "Sunday" ]
Jeremy Ronquillo3c2f10d2019-06-10 16:54:46 -0700123 if ( simulateDay == "" ){
124 today = now[ Calendar.DAY_OF_WEEK ]
125 day = dayMap[ today ]
126 print now.toString()
127 } else {
128 day = simulateDay
129 }
Jon Hall6af749d2018-05-29 12:59:47 -0700130}
Jeremy Ronquilloa37920b2019-05-23 14:34:25 -0700131
Jeremy Ronquillo336110a2019-07-11 14:20:40 -0700132def getCurrentTime(){
133 // get time of the PST zone.
134
135 TimeZone.setDefault( TimeZone.getTimeZone( 'PST' ) )
136 return new Date()
137}
138
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700139// gets ONOS branches from params or string parameter
140def getONOSBranches(){
141 echo "-> getONOSBranches()"
142 if ( manually_run ){
143 return branchesParam.tokenize( "\n;, " )
Jeremy Ronquillo64eeeb12019-05-13 11:19:46 -0700144 } else {
Jeremy Ronquillo3c2f10d2019-06-10 16:54:46 -0700145 return test_list.getBranchesFromDay( day )
Devin Lim2edfcec2018-05-09 17:16:21 -0700146 }
147}
Devin Limf5175192018-05-14 19:13:22 -0700148
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700149def getONOSTests(){
150 echo "-> getONOSTests()"
151 if ( manually_run ){
152 return test_list.getTestsFromStringList( testsParam.tokenize( "\n;, " ) )
153 } else {
Jeremy Ronquillo3c2f10d2019-06-10 16:54:46 -0700154
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700155 return test_list.getTestsFromDay( day )
Jeremy Ronquilloa37920b2019-05-23 14:34:25 -0700156 }
Devin Lim2edfcec2018-05-09 17:16:21 -0700157}
Devin Limf5175192018-05-14 19:13:22 -0700158
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700159// init paths for the files and directories.
160def initGraphPaths(){
Jeremy Ronquillo1e5d7f22019-07-17 14:18:42 -0700161 graphPaths.put( "histogramMultiple", fileRelated.rScriptPaths[ "scripts" ][ "histogramMultiple" ] )
162 graphPaths.put( "pieMultiple", fileRelated.rScriptPaths[ "scripts" ][ "pieMultiple" ] )
163 graphPaths.put( "saveDirectory", fileRelated.workspaces[ "VM" ] )
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700164}
Jeremy Ronquillo64eeeb12019-05-13 11:19:46 -0700165
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700166// **********************
167// Determine Tests to Run
168// **********************
169
170def printTestsToRun( runList ){
171 if ( manually_run ){
172 println "Tests to be run manually:"
173 } else {
174 if ( isFabric ){
175 postToSlackSR()
Devin Lim2edfcec2018-05-09 17:16:21 -0700176 }
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700177 if ( today == Calendar.MONDAY ){
178 postToSlackTestsToRun()
179 }
180 println "Defaulting to " + day + " tests:"
Devin Lim2edfcec2018-05-09 17:16:21 -0700181 }
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700182 for ( list in runList ){
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700183 echo "" + list
184 }
Devin Lim2edfcec2018-05-09 17:16:21 -0700185}
Devin Limf5175192018-05-14 19:13:22 -0700186
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700187def postToSlackSR(){
188 // If it is automated running, it will post the beginning message to the channel.
189 slackSend( channel: 'sr-failures', color: '#03CD9F',
190 message: ":sparkles:" * 16 + "\n" +
191 "Starting tests on : " + now.toString() +
192 "\n" + ":sparkles:" * 16 )
193}
194
195def postToSlackTestsToRun(){
196 slackSend( color: '#FFD988',
197 message: "Tests to be run this weekdays : \n" +
Jeremy Ronquillo336110a2019-07-11 14:20:40 -0700198 printDaysForTest() )
199}
200
201def printDaysForTest(){
202 // Print the days for what test has.
203 AllTheTests = test_list.getAllTests()
204
205 result = ""
206 for ( String test in AllTheTests.keySet() ){
207 result += test + ": ["
208 test_schedule = AllTheTests[ test ][ "schedules" ]
209 for ( String sch_dict in test_schedule ){
210 for ( String day in test_list.convertScheduleKeyToDays( sch_dict[ "branch" ] ) ){
211 result += day + " "
212 }
213 }
214 result += "]\n"
215 }
216 return result
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700217}
218
219// *********
220// Run Tests
221// *********
222
Jeremy Ronquillo336110a2019-07-11 14:20:40 -0700223def tagCheck( onos_tag, onos_branch ){
224 // check the tag for onos if it is not empty
225
226 result = "git checkout "
227 if ( onos_tag == "" ){
228 //create new local branch
229 result += onos_branch
230 }
231 else {
232 //checkout the tag
233 result += onos_tag
234 }
235 return result
236}
237
238def preSetup( onos_branch, test_branch, onos_tag, isManual ){
239 // pre setup part which will clean up and checkout to corresponding branch.
240
241 result = ""
242 if ( !isManual ){
243 result = '''echo -e "\n##### Set TestON Branch #####"
244 echo "TestON Branch is set on: ''' + test_branch + '''"
245 cd ~/OnosSystemTest/
246 git checkout HEAD~1 # Make sure you aren't pn a branch
247 git branch | grep -v "detached from" | xargs git branch -d # delete all local branches merged with remote
248 git branch -D ''' + test_branch + ''' # just in case there are local changes. This will normally result in a branch not found error
249 git clean -df # clean any local files
250 git fetch --all # update all caches from remotes
251 git reset --hard origin/''' + test_branch + ''' # force local index to match remote branch
252 git clean -df # clean any local files
253 git checkout ''' + test_branch + ''' #create new local branch
254 git branch
255 git log -1 --decorate
256 echo -e "\n##### Set ONOS Branch #####"
257 echo "ONOS Branch is set on: ''' + onos_branch + '''"
258 echo -e "\n #### check karaf version ######"
259 env |grep karaf
260 cd ~/onos
261 git checkout HEAD~1 # Make sure you aren't pn a branch
262 git branch | grep -v "detached from" | xargs git branch -d # delete all local branches merged with remote
263 git branch -D ''' + onos_branch + ''' # just incase there are local changes. This will normally result in a branch not found error
264 git clean -df # clean any local files
265 git fetch --all # update all caches from remotes
266 git reset --hard origin/''' + onos_branch + ''' # force local index to match remote branch
267 git clean -df # clean any local files
268 rm -rf buck-out
269 rm -rf bazel-*
270 ''' + tagCheck( onos_tag, onos_branch ) + '''
271 git branch
272 git log -1 --decorate
273 echo -e "\n##### set jvm heap size to 8G #####"
274 echo ${ONOSJAVAOPTS}
275 inserted_line="export JAVA_OPTS=\"\${ONOSJAVAOPTS}\""
276 sed -i "s/bash/bash\\n$inserted_line/" ~/onos/tools/package/bin/onos-service
277 echo "##### Check onos-service setting..... #####"
278 cat ~/onos/tools/package/bin/onos-service
279 export JAVA_HOME=/usr/lib/jvm/java-8-oracle'''
280 } else {
281 result = '''echo "Since this is a manual run, we'll use the current ONOS and TestON branch:"
282 echo "ONOS branch:"
283 cd ~/OnosSystemTest/
284 git branch
285 echo "TestON branch:"
286 cd ~/TestON/
287 git branch'''
288 }
289 return result
290}
291
292def postSetup( onos_branch, test_branch, onos_tag, isManual ){
293 // setup that will build ONOS
294
295 result = ""
296 if ( !isManual ){
297 result = '''echo -e "Installing bazel"
298 cd ~
299 rm -rf ci-management
300 git clone https://gerrit.onosproject.org/ci-management
301 cd ci-management/jjb/onos/
302 export GERRIT_BRANCH="''' + onos_branch + '''"
303 chmod +x install-bazel.sh
304 ./install-bazel.sh
305 '''
306 } else {
307 result = '''echo -e "Since this is a manual run, we will not install Bazel."'''
308 }
309 return result
310}
311
312def generateKey(){
313 // generate cluster-key of the onos
314
315 try {
316 sh script: '''
317 #!/bin/bash -l
318 set +e
319 . ~/.bashrc
320 env
321 onos-push-bits-through-proxy
322 onos-gen-cluster-key -f
323 ''', label: "Generate Cluster Key", returnStdout: false
324 } catch ( all ){
325 }
326}
327
328// Initialize the environment Setup for the onos and OnosSystemTest
329def envSetup( onos_branch, test_branch, onos_tag, jobOn, manuallyRun, nodeLabel ){
330 // to setup the environment using the bash script
331 stage( "Environment Setup: " + onos_branch + "-" + nodeLabel + "-" + jobOn ) {
332 // after env: ''' + borrow_mn( jobOn ) + '''
333 sh script: '''#!/bin/bash -l
334 set +e
335 . ~/.bashrc
336 env
337 ''' + preSetup( onos_branch, test_branch, onos_tag, manuallyRun ), label: "Repo Setup", returnStdout: false
338 sh script: postSetup( onos_branch, test_branch, onos_tag, manuallyRun ), label: "Install Bazel", returnStdout: false
339 generateKey()
340 }
341}
342
343// export Environment properties.
344def exportEnvProperty( onos_branch, test_branch, jobOn, wiki, tests, postResult, manually_run, onosTag, isOldFlow, nodeLabel ){
345 // export environment properties to the machine.
346
347 filePath = "/var/jenkins/TestONOS-" + jobOn + "-" + onos_branch + ".property"
348
349 stage( "Property Export: " + onos_branch + "-" + nodeLabel + "-" + jobOn ) {
350 sh script: '''
351 echo "ONOSBranch=''' + onos_branch + '''" > ''' + filePath + '''
352 echo "TestONBranch=''' + test_branch + '''" >> ''' + filePath + '''
353 echo "ONOSTag=''' + onosTag + '''" >> ''' + filePath + '''
354 echo "WikiPrefix=''' + wiki + '''" >> ''' + filePath + '''
355 echo "ONOSJAVAOPTS=''' + env.ONOSJAVAOPTS + '''" >> ''' + filePath + '''
356 echo "Tests=''' + tests + '''" >> ''' + filePath + '''
357 echo "postResult=''' + postResult + '''" >> ''' + filePath + '''
358 echo "manualRun=''' + manually_run + '''" >> ''' + filePath + '''
359 echo "isOldFlow=''' + isOldFlow + '''" >> ''' + filePath + '''
360 ''', label: "Exporting Property File: " + filePath
361 }
362}
363
364def trigger( branch, tests, nodeLabel, jobOn, manuallyRun, onosTag ){
365 // triggering function that will setup the environment and determine which pipeline to trigger
366
367 println "Job name: " + jobOn + "-pipeline-" + ( manuallyRun ? "manually" : branch )
368 def wiki = branch
369 def onos_branch = test_list.addPrefixToBranch( branch )
370 def test_branch = test_list.addPrefixToBranch( branch )
371 assignedNode = null
372 node( label: nodeLabel ) {
Jeremy Ronquillo336110a2019-07-11 14:20:40 -0700373 envSetup( onos_branch, test_branch, onosTag, jobOn, manuallyRun, nodeLabel )
374 exportEnvProperty( onos_branch, test_branch, jobOn, wiki, tests, post_result, manuallyRun, onosTag, isOldFlow, nodeLabel )
375 assignedNode = env.NODE_NAME
376 }
377
378 jobToRun = jobOn + "-pipeline-" + ( manuallyRun ? "manually" : wiki )
379 build job: jobToRun, propagate: false, parameters: [ [ $class: 'StringParameterValue', name: 'Category', value: jobOn ],
380 [ $class: 'StringParameterValue', name: 'Branch', value: branch ],
381 [ $class: 'StringParameterValue', name: 'TestStation', value: assignedNode ],
Jeremy Ronquillo6da78cf2019-07-29 11:47:19 -0700382 [ $class: 'StringParameterValue', name: 'NodeLabel', value: nodeLabel ],
383 [ $class: 'StringParameterValue', name: 'TimeOut', value: pipelineTimeOut.toString() ] ]
Jeremy Ronquillo336110a2019-07-11 14:20:40 -0700384}
385
386def trigger_pipeline( branch, tests, nodeLabel, jobOn, manuallyRun, onosTag ){
387 // nodeLabel : nodeLabel from tests.json
388 // jobOn : "SCPF" or "USECASE" or "FUNC" or "HA"
389 // this will return the function by wrapping them up with return{} to prevent them to be
390 // executed once this function is called to assign to specific variable.
391 return {
392 trigger( branch, tests, nodeLabel, jobOn, manuallyRun, onosTag )
393 }
394}
395
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700396def generateRunList(){
397 runList = [:]
Jeremy Ronquilloedb663b2019-06-26 14:09:38 -0700398 validSchedules = test_list.getValidSchedules( day )
399 echo "validSchedules: " + validSchedules
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700400 for ( branch in onos_branches ){
Jeremy Ronquilloa5aa7c12019-06-04 10:26:36 -0700401 runBranch = []
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700402 nodeLabels = test_list.getAllNodeLabels( branch, selectedTests )
403 for ( nodeLabel in nodeLabels ){
404 selectedNodeLabelTests = test_list.getTestsFromNodeLabel( nodeLabel, branch, selectedTests )
405 selectedNodeLabelCategories = test_list.getAllTestCategories( selectedNodeLabelTests )
406 for ( category in selectedNodeLabelCategories ){
Jeremy Ronquilloa5aa7c12019-06-04 10:26:36 -0700407 selectedNodeLabelCategoryTests = test_list.getTestsFromCategory( category, selectedNodeLabelTests )
Jeremy Ronquilloedb663b2019-06-26 14:09:38 -0700408
409 filteredList = [:]
410 for ( key in selectedNodeLabelCategoryTests.keySet() ){
411 for ( sch in selectedNodeLabelCategoryTests[ key ][ "schedules" ] ){
Jeremy Ronquillobd26bdb2019-07-08 16:15:45 -0700412 if ( validSchedules.contains( sch[ "day" ] ) && sch[ "branch" ] == test_list.convertBranchToBranchCode( branch ) || manually_run ){
Jeremy Ronquilloedb663b2019-06-26 14:09:38 -0700413 filteredList.put( key, selectedNodeLabelCategoryTests[ key ] )
414 break
415 }
416 }
417 }
418
419 echo "=========================================="
420 echo "BRANCH: " + branch
421 echo "CATEGORY: " + category
422 echo "TESTS: " + filteredList
423 if ( filteredList != [:] ){
424 exeTestList = test_list.getTestListAsString( filteredList )
Jeremy Ronquillo336110a2019-07-11 14:20:40 -0700425 runList.put( branch + "-" + nodeLabel + "-" + category, trigger_pipeline( branch, exeTestList, nodeLabel, category, manually_run, onos_tag ) )
Jeremy Ronquilloedb663b2019-06-26 14:09:38 -0700426 }
427
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700428 }
Jeremy Ronquilloa37920b2019-05-23 14:34:25 -0700429 }
Devin Lim2edfcec2018-05-09 17:16:21 -0700430 }
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700431 return runList
Devin Lim2edfcec2018-05-09 17:16:21 -0700432}
Devin Limf5175192018-05-14 19:13:22 -0700433
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700434def runTests(){
435 runList = generateRunList()
436 printTestsToRun( runList )
437 parallel runList
Devin Lim2edfcec2018-05-09 17:16:21 -0700438}
Devin Limf5175192018-05-14 19:13:22 -0700439
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700440// ***************
441// Generate Graphs
442// ***************
Devin Lim2edfcec2018-05-09 17:16:21 -0700443
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700444def generateGraphs(){
445 // If it is automated running, it will generate the stats graph on VM.
446 if ( !manually_run ){
447 for ( String b in onos_branches ){
Jeremy Ronquillo336110a2019-07-11 14:20:40 -0700448 graphs.generateStatGraph( "TestStation-VMs",
Jeremy Ronquillo21c29fc2019-06-05 11:15:24 -0700449 test_list.addPrefixToBranch( b ),
Jeremy Ronquillo96e2bd32019-05-28 15:40:18 -0700450 graphPaths[ "histogramMultiple" ],
451 graphPaths[ "pieMultiple" ],
452 graphPaths[ "saveDirectory" ] )
Jeremy Ronquilloa37920b2019-05-23 14:34:25 -0700453 }
Devin Lim2edfcec2018-05-09 17:16:21 -0700454 }
Devin Lim2edfcec2018-05-09 17:16:21 -0700455}