AbstractCliTestCase.groovy
001 /*
002  * Copyright 2009-2013 the original author or authors.
003  *
004  * Licensed under the Apache License, Version 2.0 (the "License");
005  * you may not use this file except in compliance with the License.
006  * You may obtain a copy of the License at
007  *
008  *      http://www.apache.org/licenses/LICENSE-2.0
009  *
010  * Unless required by applicable law or agreed to in writing, software
011  * distributed under the License is distributed on an "AS IS" BASIS,
012  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013  * See the License for the specific language governing permissions and
014  * limitations under the License.
015  */
016 
017 package griffon.test
018 
019 import griffon.util.BuildSettingsHolder
020 
021 import java.util.concurrent.TimeUnit
022 import java.util.concurrent.locks.Condition
023 import java.util.concurrent.locks.Lock
024 import java.util.concurrent.locks.ReentrantLock
025 
026 /**
027 * This abstract test case makes it easy to run a Griffon command and
028 * query its output. It's currently configured via a set of system
029 * properties:
030 <ul>
031 <li><tt>griffon.home</tt> - location of Griffon distribution to test</li>
032 <li><tt>griffon.version</tt> - version of Griffon we're testing</li>
033 <li><tt>griffon.cli.work.dir</tt> - location of the test case's working directory</li>
034 </ul>
035 *
036 @author Peter Ledbrook (Grails 1.1)
037 */
038 abstract class AbstractCliTestCase extends GroovyTestCase {
039     private final Lock lock = new ReentrantLock()
040     private final Condition condition = lock.newCondition()
041  
042     private String commandOutput
043     private String griffonHome = System.getProperty('griffon.home') ?: BuildSettingsHolder.settings?.griffonHome?.absolutePath
044     private String griffonVersion = System.getProperty('griffon.version') ?: BuildSettingsHolder.settings?.griffonVersion
045     private File workDir = new File(System.getProperty('griffon.cli.work.dir') ?: '.')
046  
047     private Process process
048     private boolean streamsProcessed
049  
050     File outputDir = new File(BuildSettingsHolder.settings?.projectTargetDir ?: new File('target'), 'cli-output')
051     long timeout = 60 1000 // min * sec/min * ms/sec
052  
053     /**
054      * Executes a Griffon command. The path to the Griffon script is
055      * inserted at the front, so the first element of <tt>command</tt>
056      * should be the name of the Griffon command you want to start,
057      * e.g. "help" or "run-app".
058      @param a list of command arguments (minus the Griffon script/executable).
059      */
060     protected void execute(List<String> command) {
061         // Make sure the working and output directories exist before
062         // running the command.
063         workDir.mkdirs()
064         outputDir.mkdirs()
065  
066         // Add the path to the Griffon script as the first element of
067         // the command. Note that we use an absolute path.
068         def cmd = [] // new ArrayList<String>(command.size() + 2)
069         cmd.add "${griffonHome}/bin/griffon".toString()
070         if (System.getProperty('griffon.work.dir')) {
071             cmd.add "-Dgriffon.work.dir=${System.getProperty('griffon.work.dir')}".toString()
072         }
073         cmd.addAll command
074  
075         // Prepare to execute Griffon as a separate process in the
076         // configured working directory.
077         def pb = new ProcessBuilder(cmd)
078         pb.redirectErrorStream(true)
079         pb.directory(workDir)
080         pb.environment()['GRIFFON_HOME'] = griffonHome
081         
082         process = pb.start()
083  
084         // Read the process output on a separate thread. This is
085         // necessary to deal with output that overflows the buffer
086         // and when a command requires user input at some stage.
087         final currProcess = process
088         Thread.startDaemon {
089             output = currProcess.in.text
090  
091             // Once we've finished reading the process output, signal
092             // the main thread.
093             signalDone()
094         }
095     }
096  
097     /**
098      * Returns the process output as a string.
099      */
100     String getOutput() {
101         return commandOutput
102     }
103  
104     void setOutput(String output) {
105         this.commandOutput = output
106     }
107  
108     /**
109      * Returns the working directory for the current command. This
110      * may be the base working directory or a project.
111      */
112     File getWorkDir() {
113         return workDir
114     }
115  
116     void setWorkDir(File dir) {
117         this.workDir = dir
118     }
119  
120     /**
121      * Allows you to provide user input for any commands that require
122      * it. In other words, you can run commands in interactive mode.
123      * For example, you could pass "app1" as the <tt>input</tt> parameter
124      * when running the "create-app" command.
125      */
126     void enterInput(String input) {
127         process << input << '\r'
128     }
129  
130     /**
131      * Waits for the current command to finish executing. It returns
132      * the exit code from the external process. It also dumps the
133      * process output into the "cli-tests/output" directory to aid
134      * debugging.
135      */
136     int waitForProcess() {
137         // Interrupt the main thread if we hit the timeout.
138         final mainThread = Thread.currentThread()
139         final timeout = this.timeout
140         final timeoutThread = Thread.startDaemon {
141             try {
142                 Thread.sleep(timeout)
143                 
144                 // Timed out. Interrupt the main thread.
145                 mainThread.interrupt()
146             }
147             catch (InterruptedException ex) {
148                 // We're expecting this interruption.
149             }
150         }
151  
152         // First wait for the process to finish.
153         int code
154         try {
155             code = process.waitFor()
156  
157             // Process completed normally, so kill the timeout thread.
158             timeoutThread.interrupt()
159         }
160         catch (InterruptedException ex) {
161             code = 111
162  
163             // The process won't finish, so we shouldn't wait for the
164             // output stream to be processed.
165             lock.lock()
166             streamsProcessed = true
167             lock.unlock()
168 
169              // Now kill the process since it appears to be stuck.
170              process.destroy()
171         }
172  
173         // Now wait for the stream reader threads to finish.
174         lock.lock()
175         try {
176             while (!streamsProcessedcondition.await(2, TimeUnit.MINUTES)
177         }
178         finally {
179             lock.unlock()
180         }
181  
182         // DEBUG - Dump the process output to a file.
183         int i = 1
184         def outFile = new File(outputDir, "${getClass().simpleName}-out-${i}.txt")
185         while (outFile.exists()) {
186             i++
187             outFile = new File(outputDir, "${getClass().simpleName}-out-${i}.txt")
188         }
189         outFile << commandOutput
190         // END DEBUG
191  
192         return code
193     }
194  
195     /**
196      * Signals any threads waiting on <tt>condition</tt> to inform them
197      * that the process output stream has been read. Should only be used
198      * by this class (not sub-classes). It's protected so that it can be
199      * called from the reader thread closure (some strange Groovy behaviour).
200      */
201     protected void signalDone() {
202         // Signal waiting threads that we're done.
203         lock.lock()
204         try {
205             streamsProcessed = true
206             condition.signalAll()
207         }
208         finally {
209             lock.unlock()
210         }
211     }
212  
213     /**
214      * Checks that the output of the current command starts with the
215      * expected header, which includes the Griffon version and the
216      * location of GRIFFON_HOME.
217      */
218     protected final void verifyHeader() {
219         assertTrue output.startsWith("""Welcome to Griffon ${griffonVersion} - http://griffon-framework.org/
220 Licensed under Apache Standard License 2.0
221 Griffon home is set to: ${griffonHome}
222 """)
223     }
224 }