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