griffon/test/AbstractCliTestCase.groovy

`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 = 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 }`