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