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 = 2 * 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 (!streamsProcessed) condition.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 }
|