com pftt2: interim http testing support: README.mdown bin/pftt.cmd doc/index.html hs_err_pid22876.log src/com/mo stc/pftt/host/ExecOutput.java
src/com/mostc/pftt/host/Host .java src/com/mostc/pftt/host/LocalHost.java src/com/mo stc/pftt/host/SSHHost.java src/com/mostc/pftt/main/PfttMain .java
src/com/mostc/pftt/model/phpt/EPhptTestStatus.java src/com/mostc/pftt/model/phpt/PhpBuild.java src/com/most c/pftt/model/phpt/PhpIni.java
src/com/mostc/pftt/mode
| From: | Matt Ficken | Date: | Fri, 26 Oct 2012 06:32:11 +0000 |
| Subject: | com pftt2: interim http testing support: README.mdown bin/pftt.cmd doc/index.html hs_err_pid22876.log src/com/mo stc/pftt/host/ExecOutput.java src/com/mostc/pftt/host/Host .java src/com/mostc/pftt/host/LocalHost.java src/com/mo stc/pftt/host/SSHHost.java src/com/mostc/pftt/main/PfttMain .java src/com/mostc/pftt/model/phpt/EPhptTestStatus.java src/com/mostc/pftt/model/phpt/PhpBuild.java src/com/most c/pftt/model/phpt/PhpIni.java src/com/mostc/pftt/mode |
||
| Groups: | php.qa | ||
| Request: | Send a blank email to php-qa+get-67215@lists.php.net to get a copy of this message | ||
Commit: 6d3e45bd8912aa7fb52d0765ff2dfff147c9591f
Author: Matt Ficken <v-mafick@microsoft.com> Thu, 25 Oct 2012 23:32:11 -0700
Parents: 3ffd21bc672b8a20703d935d86dd4261870308f9
Branches: master
Link: http://git.php.net/?p=pftt2.git;a=commitdiff;h=6d3e45bd8912aa7fb52d0765ff2dfff147c9591f
Log:
interim http testing support
Changed paths:
M README.mdown
M bin/pftt.cmd
M doc/index.html
A hs_err_pid22876.log
M src/com/mostc/pftt/host/ExecOutput.java
M src/com/mostc/pftt/host/Host.java
M src/com/mostc/pftt/host/LocalHost.java
M src/com/mostc/pftt/host/SSHHost.java
M src/com/mostc/pftt/main/PfttMain.java
M src/com/mostc/pftt/model/phpt/EPhptTestStatus.java
M src/com/mostc/pftt/model/phpt/PhpBuild.java
M src/com/mostc/pftt/model/phpt/PhpIni.java
M src/com/mostc/pftt/model/phpt/PhptTestCase.java
A src/com/mostc/pftt/model/sapi/ApacheManager.java
A src/com/mostc/pftt/model/sapi/BuiltinWebServerInstance.java
A src/com/mostc/pftt/model/sapi/BuiltinWebServerManager.java
A src/com/mostc/pftt/model/sapi/CrashedWebServerInstance.java
A src/com/mostc/pftt/model/sapi/IISManager.java
A src/com/mostc/pftt/model/sapi/SAPIInstance.java
A src/com/mostc/pftt/model/sapi/SAPIManager.java
A src/com/mostc/pftt/model/sapi/TestCaseGroupKey.java
A src/com/mostc/pftt/model/sapi/WebServerInstance.java
A src/com/mostc/pftt/model/sapi/WebServerManager.java
A src/com/mostc/pftt/model/sapi/package.html
M src/com/mostc/pftt/runner/AbstractPhptTestCaseRunner.java
M src/com/mostc/pftt/runner/AbstractPhptTestCaseRunner2.java
M src/com/mostc/pftt/runner/CliPhptTestCaseRunner.java
D src/com/mostc/pftt/runner/HTTPTestCaseRunner.java
M src/com/mostc/pftt/runner/PhptTestPackRunner.java
A src/com/mostc/pftt/runner/PhptTestPackRunner.java.bak
M src/com/mostc/pftt/scenario/AbstractApacheScenario.java
M src/com/mostc/pftt/scenario/AbstractIISScenario.java
M src/com/mostc/pftt/scenario/AbstractSAPIScenario.java
M src/com/mostc/pftt/scenario/AbstractWebServerScenario.java
D src/com/mostc/pftt/scenario/BuiltinWWWScenario.java
A src/com/mostc/pftt/scenario/BuiltinWebServerScenario.java
M src/com/mostc/pftt/scenario/CLIScenario.java
M src/com/mostc/pftt/scenario/IISExpressFastCGIScenario.java
M src/com/mostc/pftt/scenario/IISStandardFastCGIScenario.java
M src/com/mostc/pftt/scenario/Scenario.java
M src/com/mostc/pftt/scenario/ScenarioSet.java
M src/com/mostc/pftt/telemetry/PhptTelemetryReader.java
M src/com/mostc/pftt/telemetry/PhptTelemetryWriter.java
M src/com/mostc/pftt/telemetry/PhptTestResult.java
M src/com/mostc/pftt/ui/ExpectedActualDiffPHPTDisplay.java
M src/com/mostc/pftt/ui/PhptHostTab.java
M src/com/mostc/pftt/util/StringUtil.java
M src/com/mostc/pftt/util/WindowsSnapshotDownloadUtil.groovy
diff --git a/README.mdown b/README.mdown index c3300bb..29c271e 100644 --- a/README.mdown +++ b/README.mdown @@ -23,6 +23,7 @@ BSD ### Important Classes - [CliPhptTestCaseRunner](doc/apidoc/com/mostc/pftt/runner/CliPhptTestCaseRunner.html) - Executes a single PHPT Test Case (using the CLI SAPI) and evaluates + - [HttpTestCaseRunner](doc/apidoc/com/mostc/pftt/runner/HttpTestCaseRunner.html) - Executes a single PHPT Test Case against a PHP build running under a Web Server (Builtin, IIS, Apache) - [Host](doc/apidoc/com/mostc/pftt/host/Host.html) - Abstracts host management so client code doesn't need to care if host is local or remote(ssh) - [ScenarioSet](doc/apidoc/com/mostc/pftt/scenario/ScenarioSet.html) - Models 90 sets of different [Scenarios](apidoc/com/mostc/pftt/scenario/Scenario.html) to test PHP in - [PhptTestCase](doc/apidoc/com/mostc/pftt/model/phpt/PhptTestCase.html) - Models a PHPT Test case diff --git a/bin/pftt.cmd b/bin/pftt.cmd index c53c737..a8b576c 100644 --- a/bin/pftt.cmd +++ b/bin/pftt.cmd @@ -1,21 +1,21 @@ -@echo off +@ECHO off REM script for running PFTT on Windows -set PFTT_HOME=C:\php-sdk\PFTT\current -set PFTT_LIB=%PFTT_HOME%\lib +SET PFTT_HOME=C:\php-sdk\PFTT\current +SET PFTT_LIB=%PFTT_HOME%\lib -set CLASSPATH=%PFTT_HOME%\build;%PFTT_LIB%\htmlcleaner-2.2.jar;%PFTT_LIB%\groovy-1.8.6.jar;%PFTT_LIB%\icu4j-49_1.jar;%PFTT_LIB%\icudata.jar;%PFTT_LIB%\icutzdata.jar;%PFTT_LIB%\j2ssh-common-0.2.9.jar;%PFTT_LIB%\j2ssh-core-0.2.9.jar;%PFTT_LIB%\jansi-1.7.jar;%PFTT_LIB%\jline-0.9.94.jar;%PFTT_LIB%\jzlib-1.0.7.jar;%PFTT_LIB%\selenium-server-standalone-2.19.0.jar;%PFTT_LIB%\xercesImpl.jar;%PFTT_LIB%\xmlpull-1.1.3.1.jar;%PFTT_LIB%\commons-net-3.1.jar;%PFTT_LIB%\commons-cli-1.2.jar;%PFTT_LIB%\antlr-2.7.7.jar;%PFTT_LIB%\asm-3.2.jar;%PFTT_LIB%\asm-analysis-3.2.jar;%PFTT_LIB%\asm-commons-3.2.jar;%PFTT_LIB%\asm-tree-3.2.jar;%PFTT_LIB%\asm-util-3.2.jar +SET CLASSPATH=%PFTT_HOME%\build;%PFTT_LIB%\htmlcleaner-2.2.jar;%PFTT_LIB%\groovy-1.8.6.jar;%PFTT_LIB%\icu4j-49_1.jar;%PFTT_LIB%\icudata.jar;%PFTT_LIB%\icutzdata.jar;%PFTT_LIB%\j2ssh-common-0.2.9.jar;%PFTT_LIB%\j2ssh-core-0.2.9.jar;%PFTT_LIB%\jansi-1.7.jar;%PFTT_LIB%\jline-0.9.94.jar;%PFTT_LIB%\jzlib-1.0.7.jar;%PFTT_LIB%\selenium-server-standalone-2.19.0.jar;%PFTT_LIB%\xercesImpl.jar;%PFTT_LIB%\xmlpull-1.1.3.1.jar;%PFTT_LIB%\commons-net-3.1.jar;%PFTT_LIB%\commons-cli-1.2.jar;%PFTT_LIB%\antlr-2.7.7.jar;%PFTT_LIB%\asm-3.2.jar;%PFTT_LIB%\asm-analysis-3.2.jar;%PFTT_LIB%\asm-commons-3.2.jar;%PFTT_LIB%\asm-tree-3.2.jar;%PFTT_LIB%\asm-util-3.2.jar REM find java and execute PfttMain REM search %PATH% for java WHERE java > NUL -if %ERRORLEVEL% EQU 0 ( +IF %ERRORLEVEL% EQU 0 ( java -classpath %CLASSPATH% com.mostc.pftt.main.PfttMain %* ) ELSE ( REM check for JAVA_HOME - IF EXIST %JAVA_HOME%\lib\tools.jar ( + IF EXIST "%JAVA_HOME%\lib\tools.jar" ( %JAVA_HOME%\bin\java -classpath %CLASSPATH% com.mostc.pftt.main.PfttMain %* - ) ELSE ( + ) ELSE ( ECHO user error set JAVA_HOME or add java to PATH and try again ) ) diff --git a/doc/index.html b/doc/index.html index c13f472..d08d6ca 100644 --- a/doc/index.html +++ b/doc/index.html @@ -26,6 +26,7 @@ PHPUnit tests for PHP Applications, along with comparison, debugging, performanc <h2>Important Classes</h2> <p><a href="apidoc/com/mostc/pftt/runner/CliPhptTestCaseRunner.html">CliPhptTestCaseRunner</a> - Executes a single PHPT Test Case (using the CLI SAPI) and evaluates</p> +<p><a href="apidoc/com/mostc/pftt/runner/HttpTestCaseRunner.html">HttpTestCaseRunner</a> - Executes a single PHPT Test Case against a PHP build running under a Web Server (Builtin, IIS, Apache)</p> <p><a href="apidoc/com/mostc/pftt/host/Host.html">Host</a> - Abstracts host management so client code doesn't need to care if host is local or remote(ssh)</p> <p><a href="apidoc/com/mostc/pftt/scenario/ScenarioSet.html">ScenarioSet</a> - Models 90 sets of different <a href="apidoc/com/mostc/pftt/scenario/Scenario.html">Scenario</a>s to test PHP in</p> <p><a href="apidoc/com/mostc/pftt/model/phpt/PhptTestCase.html">PhptTestCase</a> - Models a PHPT Test case</p> diff --git a/hs_err_pid22876.log b/hs_err_pid22876.log new file mode 100644 index 0000000..1f8623c --- /dev/null +++ b/hs_err_pid22876.log @@ -0,0 +1,149 @@ +# +# There is insufficient memory for the Java Runtime Environment to continue. +# Native memory allocation (malloc) failed to allocate 32744 bytes for ChunkPool::allocate +# Possible reasons: +# The system is out of physical RAM or swap space +# In 32 bit mode, the process size limit was hit +# Possible solutions: +# Reduce memory load on the system +# Increase physical memory or swap space +# Check if swap backing store is full +# Use 64 bit Java on a 64 bit OS +# Decrease Java heap size (-Xmx/-Xms) +# Decrease number of Java threads +# Decrease Java thread stack sizes (-Xss) +# Set larger code cache with -XX:ReservedCodeCacheSize= +# This output file may be truncated or incomplete. +# +# Out of Memory Error (allocation.cpp:211), pid=22876, tid=8040 +# +# JRE version: 6.0_35-b10 +# Java VM: Java HotSpot(TM) 64-Bit Server VM (20.10-b01 mixed mode windows-amd64 compressed oops) + +--------------- T H R E A D --------------- + +Current thread (0x0000000005d85800): JavaThread "C2 CompilerThread1" daemon [_thread_in_native, id=8040, stack(0x0000000006d60000,0x0000000006e60000)] + +Stack: [0x0000000006d60000,0x0000000006e60000] + +Current CompileTask: +C2:1155916 9% java.util.AbstractCollection.contains(Ljava/lang/Object;)Z @ 29 (55 bytes) + + +--------------- P R O C E S S --------------- + +Java Threads: ( => current thread ) + 0x000000000724b000 JavaThread "AWT-EventQueue-0" [_thread_blocked, id=17572, stack(0x0000000009b10000,0x0000000009c10000)] + 0x0000000008283000 JavaThread "AWT-Windows" daemon [_thread_in_native, id=14188, stack(0x0000000009280000,0x0000000009380000)] + 0x00000000081a7000 JavaThread "AWT-Shutdown" [_thread_blocked, id=2360, stack(0x0000000009180000,0x0000000009280000)] + 0x0000000008658800 JavaThread "Java2D Disposer" daemon [_thread_blocked, id=27840, stack(0x0000000009080000,0x0000000009180000)] + 0x00000000071b1800 JavaThread "Timer-1" [_thread_blocked, id=12960, stack(0x0000000007560000,0x0000000007660000)] + 0x0000000007152000 JavaThread "Timer-0" [_thread_blocked, id=5892, stack(0x0000000007460000,0x0000000007560000)] + 0x0000000005da7800 JavaThread "Low Memory Detector" daemon [_thread_blocked, id=15100, stack(0x0000000006e60000,0x0000000006f60000)] +=>0x0000000005d85800 JavaThread "C2 CompilerThread1" daemon [_thread_in_native, id=8040, stack(0x0000000006d60000,0x0000000006e60000)] + 0x0000000005d6f800 JavaThread "C2 CompilerThread0" daemon [_thread_blocked, id=8600, stack(0x0000000006c60000,0x0000000006d60000)] + 0x0000000005d6c800 JavaThread "Attach Listener" daemon [_thread_blocked, id=3872, stack(0x0000000006b60000,0x0000000006c60000)] + 0x0000000005d6b800 JavaThread "Signal Dispatcher" daemon [_thread_blocked, id=13372, stack(0x0000000006a60000,0x0000000006b60000)] + 0x0000000005d1a000 JavaThread "Finalizer" daemon [_thread_blocked, id=3904, stack(0x0000000006960000,0x0000000006a60000)] + 0x0000000005d13800 JavaThread "Reference Handler" daemon [_thread_blocked, id=13252, stack(0x0000000006860000,0x0000000006960000)] + 0x000000000033b800 JavaThread "main" [_thread_in_native, id=16256, stack(0x0000000002040000,0x0000000002140000)] + +Other Threads: + 0x0000000005d08000 VMThread [stack: 0x0000000006760000,0x0000000006860000] [id=19792] + 0x0000000005db1800 WatcherThread [stack: 0x0000000006f60000,0x0000000007060000] [id=14948] + +VM state:not at safepoint (normal execution) + +VM Mutex/Monitor currently owned by a thread: None + +Heap + PSYoungGen total 164800K, used 107238K [0x00000000f5560000, 0x00000000fffa0000, 0x0000000100000000) + eden space 155648K, 66% used [0x00000000f5560000,0x00000000fb9d99c0,0x00000000fed60000) + from space 9152K, 47% used [0x00000000ff6b0000,0x00000000ffaf0000,0x00000000fffa0000) + to space 9344K, 0% used [0x00000000fed60000,0x00000000fed60000,0x00000000ff680000) + PSOldGen total 99712K, used 87086K [0x00000000e0000000, 0x00000000e6160000, 0x00000000f5560000) + object space 99712K, 87% used [0x00000000e0000000,0x00000000e550b860,0x00000000e6160000) + PSPermGen total 33664K, used 20753K [0x00000000dae00000, 0x00000000dcee0000, 0x00000000e0000000) + object space 33664K, 61% used [0x00000000dae00000,0x00000000dc2445e0,0x00000000dcee0000) + +Code Cache [0x0000000002140000, 0x00000000023b0000, 0x0000000005140000) + total_blobs=749 nmethods=292 adapters=410 free_code_cache=48972992 largest_free_block=14336 + +Dynamic libraries: +0x0000000000400000 - 0x000000000042f000 C:\Program Files\Java\jre6\bin\javaw.exe +0x00000000775e0000 - 0x000000007778b000 C:\Windows\SYSTEM32\ntdll.dll +0x00000000773c0000 - 0x00000000774df000 C:\Windows\system32\kernel32.dll +0x000007fefd8a0000 - 0x000007fefd90b000 C:\Windows\system32\KERNELBASE.dll +0x000007fefdd40000 - 0x000007fefde1b000 C:\Windows\system32\ADVAPI32.dll +0x000007fefd940000 - 0x000007fefd9df000 C:\Windows\system32\msvcrt.dll +0x000007fefe000000 - 0x000007fefe01f000 C:\Windows\SYSTEM32\sechost.dll +0x000007feff380000 - 0x000007feff4ae000 C:\Windows\system32\RPCRT4.dll +0x00000000774e0000 - 0x00000000775da000 C:\Windows\system32\USER32.dll +0x000007fefe0b0000 - 0x000007fefe117000 C:\Windows\system32\GDI32.dll +0x000007fefe020000 - 0x000007fefe02e000 C:\Windows\system32\LPK.dll +0x000007feff180000 - 0x000007feff24a000 C:\Windows\system32\USP10.dll +0x000007fefd910000 - 0x000007fefd93e000 C:\Windows\system32\IMM32.DLL +0x000007feff710000 - 0x000007feff819000 C:\Windows\system32\MSCTF.dll +0x000000006d800000 - 0x000000006dfb9000 C:\Program Files\Java\jre6\bin\server\jvm.dll +0x000007fef8d20000 - 0x000007fef8d5b000 C:\Windows\system32\WINMM.dll +0x000000006d770000 - 0x000000006d77e000 C:\Program Files\Java\jre6\bin\verify.dll +0x000000006d3b0000 - 0x000000006d3d7000 C:\Program Files\Java\jre6\bin\java.dll +0x000000006d7c0000 - 0x000000006d7d2000 C:\Program Files\Java\jre6\bin\zip.dll +0x000007fefd3d0000 - 0x000007fefd427000 C:\Windows\system32\apphelp.dll +0x000000006d000000 - 0x000000006d1c4000 C:\Program Files\Java\jre6\bin\awt.dll +0x000007fef9dd0000 - 0x000007fef9e40000 C:\Windows\system32\WINSPOOL.DRV +0x000007fefef50000 - 0x000007feff151000 C:\Windows\system32\ole32.dll +0x000007fefe1c0000 - 0x000007fefef46000 C:\Windows\system32\SHELL32.dll +0x000007feff870000 - 0x000007feff8e1000 C:\Windows\system32\SHLWAPI.dll +0x000007fefbca0000 - 0x000007fefbe94000 C:\Windows\WinSxS\amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.7600.16385_none_fa645303170382f6\COMCTL32.dll +0x000007fefb850000 - 0x000007fefb868000 C:\Windows\system32\DWMAPI.DLL +0x000007fefbc20000 - 0x000007fefbc76000 C:\Windows\system32\uxtheme.dll +0x000007fefd430000 - 0x000007fefd43f000 C:\Windows\system32\CRYPTBASE.dll +0x000000006d2a0000 - 0x000000006d307000 C:\Program Files\Java\jre6\bin\fontmanager.dll +0x000007fefe120000 - 0x000007fefe1b8000 C:\Windows\system32\COMDLG32.DLL +0x000007fef2220000 - 0x000007fef22fc000 C:\Windows\system32\spool\DRIVERS\x64\3\unidrvui.dll +0x000007fefc800000 - 0x000007fefc80c000 C:\Windows\system32\VERSION.dll +0x000007fefda40000 - 0x000007fefdb17000 C:\Windows\system32\OLEAUT32.dll +0x000007fef6b40000 - 0x000007fef6b7a000 C:\Windows\system32\spool\DRIVERS\x64\3\mxdwdui.DLL +0x000007fef2320000 - 0x000007fef23d2000 C:\Windows\system32\spool\DRIVERS\x64\3\mxdwdrv.dll +0x000000006d610000 - 0x000000006d627000 C:\Program Files\Java\jre6\bin\net.dll +0x000007feff820000 - 0x000007feff86d000 C:\Windows\system32\WS2_32.dll +0x000007fefda30000 - 0x000007fefda38000 C:\Windows\system32\NSI.dll +0x000007fefcd70000 - 0x000007fefcdc4000 C:\Windows\system32\mswsock.dll +0x000007fefcd60000 - 0x000007fefcd67000 C:\Windows\System32\wship6.dll +0x000000006d630000 - 0x000000006d63b000 C:\Program Files\Java\jre6\bin\nio.dll +0x000000006d210000 - 0x000000006d238000 C:\Program Files\Java\jre6\bin\dcpr.dll +0x000007fefc6e0000 - 0x000007fefc6e7000 C:\Windows\System32\wshtcpip.dll +0x000007fefcbf0000 - 0x000007fefcc4b000 C:\Windows\system32\DNSAPI.dll +0x000007fefc7d0000 - 0x000007fefc7f7000 C:\Windows\system32\IPHLPAPI.DLL +0x000007fefc7c0000 - 0x000007fefc7cb000 C:\Windows\system32\WINNSI.DLL +0x000007fefa240000 - 0x000007fefa248000 C:\Windows\system32\rasadhlp.dll +0x000007fefae30000 - 0x000007fefae83000 C:\Windows\System32\fwpuclnt.dll +0x00000000777b0000 - 0x00000000777b7000 C:\Windows\system32\PSAPI.DLL + +VM Arguments: +jvm_args: -Dfile.encoding=Cp1252 +java_command: com.mostc.pftt.main.PfttMain -gui phpt_all C:\php-sdk\php-5.3-nts-windows-vc9-x86-rda541ff C:\php-sdk\php-test-pack-5.3-nts-windows-vc9-x86-rda541ff +Launcher Type: SUN_STANDARD + +Environment Variables: +PATH=C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\ +USERNAME=matt +OS=Windows_NT +PROCESSOR_IDENTIFIER=Intel64 Family 6 Model 42 Stepping 7, GenuineIntel + + + +--------------- S Y S T E M --------------- + +OS: Windows 7 , 64 bit Build 7600 + +CPU:total 8 (8 cores per cpu, 1 threads per core) family 6 model 42 stepping 7, cmov, cx8, fxsr, mmx, sse, sse2, sse3, ssse3 + +Memory: 4k page, physical 2096696k(488200k free), swap 8386784k(1692k free) + +vm_info: Java HotSpot(TM) 64-Bit Server VM (20.10-b01) for windows-amd64 JRE (1.6.0_35-b10), built on Aug 28 2012 16:50:10 by "java_re" with MS VC++ 8.0 (VS2005) + +time: Wed Oct 24 13:21:55 2012 +elapsed time: 1155 seconds + diff --git a/src/com/mostc/pftt/host/ExecOutput.java b/src/com/mostc/pftt/host/ExecOutput.java index 003bba3..05f5d17 100644 --- a/src/com/mostc/pftt/host/ExecOutput.java +++ b/src/com/mostc/pftt/host/ExecOutput.java @@ -29,14 +29,14 @@ public class ExecOutput { public boolean isEmpty() { return StringUtil.isEmpty(output); } - public boolean isCrash() { + public boolean isCrashed() { return exit_code != 0; } public ExecOutput printOutputIfCrash() { return printOutputIfCrash(System.err); } public ExecOutput printOutputIfCrash(PrintStream ps) { - if (isCrash()) + if (isCrashed()) ps.println(output); return this; } diff --git a/src/com/mostc/pftt/host/Host.java b/src/com/mostc/pftt/host/Host.java index 0db68df..443bb71 100644 --- a/src/com/mostc/pftt/host/Host.java +++ b/src/com/mostc/pftt/host/Host.java @@ -400,6 +400,42 @@ public abstract class Host { public ExecOutput execElevated(String cmd, int timeout_sec, String chdir) throws Exception { return execElevated(cmd, timeout_sec, null, null, null, chdir, null, NO_TIMEOUT); } + public static abstract class ExecHandle { + public abstract void close(); + public abstract boolean isRunning(); + public abstract boolean isCrashed(); + public abstract String getOutput(); + public abstract int getExitCode(); + } + public ExecHandle execThread(String commandline) throws Exception { + return execThread(commandline, null, null, null); + } + public ExecHandle execThread(String commandline, byte[] stdin_data) throws Exception { + return execThread(commandline, null, null, stdin_data); + } + public ExecHandle execThread(String commandline, String chdir) throws Exception { + return execThread(commandline, null, chdir, null); + } + public ExecHandle execThread(String commandline, String chdir, byte[] stdin_data) throws Exception { + return execThread(commandline, null, chdir, stdin_data); + } + public ExecHandle execThread(String commandline, Map<String,String> env, byte[] stdin_data) throws Exception { + return execThread(commandline, env, null, stdin_data); + } + public ExecHandle execThread(String commandline, Map<String,String> env, String chdir) throws Exception { + return execThread(commandline, env, chdir, null); + } + /** executes command, returning immediately before it ends, returning a handle to asynchronously monitor + * the process. + * + * @param commandline + * @param env + * @param chdir + * @param stdin_data + * @return + * @throws Exception + */ + public abstract ExecHandle execThread(String commandline, Map<String,String> env, String chdir, byte[] stdin_data) throws Exception; public ExecOutput exec(String cmd, int timeout_sec, Map<String,String> object, byte[] stdin_post, Charset charset) throws IllegalStateException, Exception { return exec(cmd, timeout_sec, object, stdin_post, charset, (String)null); } @@ -524,12 +560,60 @@ public abstract class Host { // TODO } + /** gets a string of info about the host + * + * @return + * @throws Exception + */ + public String getSystemInfo() throws Exception { + if (isWindows()) + return exec("systeminfo", ONE_MINUTE).output; + else + return exec("uname -a", ONE_MINUTE).output; + } + + /** finds path to program on host + * + * @param cmd + * @return + * @throws Exception + */ + public String where(String cmd) throws Exception { + return isWindows() ? cmd("WHERE "+cmd, ONE_MINUTE).output : exec("WHICH "+cmd, ONE_MINUTE).output; + } + + /** returns true if command or program is found + * + * if false, passing this command to #exec will result in error because program can't be found + * + * @param cmd + * @return + */ public boolean hasCmd(String cmd) { try { - return isWindows() ? cmd("WHERE "+cmd, ONE_MINUTE).isNotEmpty() : exec("WHICH "+cmd, ONE_MINUTE).isNotEmpty(); + return StringUtil.isNotEmpty(where(cmd)); } catch ( Exception ex ) { return false; } } + + /** returns an address representing this host on one of the networks its on + * + * returns null if host is not on any network at all + * + * @return + */ + @Nullable + public abstract String getAddress(); + + /** servers running on Windows won't accept sockets from localhost to 127.0.0.1|localhost. + * + * instead have to use the ip address of one of its network interfaces (Windows doesn't have a Loopback(LO) network device) + * + * @return + */ + public String getLocalhostServableAddress() { + return isWindows() ? getAddress() : "127.0.0.1"; + } } // end public abstract class Host diff --git a/src/com/mostc/pftt/host/LocalHost.java b/src/com/mostc/pftt/host/LocalHost.java index f9a08e3..1b8f94b 100644 --- a/src/com/mostc/pftt/host/LocalHost.java +++ b/src/com/mostc/pftt/host/LocalHost.java @@ -8,8 +8,11 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; import java.net.UnknownHostException; import java.nio.charset.Charset; +import java.util.Enumeration; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; @@ -111,32 +114,28 @@ public class LocalHost extends Host { return exec(commandline, timeout, env, stdin, charset, chdir, null, NO_TIMEOUT); } @Override - public ExecOutput exec(String commandline, int timeout, Map<String,String> env, byte[] stdin, Charset charset, String chdir, TestPackRunnerThread thread, int thread_slow_sec) throws Exception { - HashMap<String,String> bin_env = new HashMap<String,String>(); - if (env!=null) { - for(String key : env.keySet()) { - String value = env.get(key); - bin_env.put(key, ""+value); - } - } - + public LocalExecHandle execThread(String commandline, Map<String,String> env, String chdir, byte[] stdin_data) throws Exception { + return exec_impl(splitCmdString(commandline), env, chdir, NO_TIMEOUT, stdin_data); + } + @Override + public ExecOutput exec(String commandline, int timeout, Map<String,String> env, byte[] stdin_data, Charset charset, String chdir, TestPackRunnerThread thread, int thread_slow_sec) throws Exception { ThreadSlowTask task = null; if (thread!=null && thread_slow_sec>NO_TIMEOUT) { task = new ThreadSlowTask(thread); timer.schedule(task, thread_slow_sec * 1000); } - LocalExecHandle eh = exec_impl(splitCmdString(commandline), bin_env, chdir, timeout, stdin); + LocalExecHandle eh = exec_impl(splitCmdString(commandline), env, chdir, timeout, stdin_data); - Object[] ret = eh.run_read_streams(charset); + eh.run(charset); if (task!=null) task.cancel(); ExecOutput out = new ExecOutput(); - out.output = (String) ret[0]; + out.output = eh.getOutput(); out.charset = eh.charset; - out.exit_code = eh.process.exitValue(); + out.exit_code = eh.getExitCode(); return out; } @@ -186,13 +185,14 @@ public class LocalHost extends Host { return System.getProperty("user.name"); } - public static class LocalExecHandle { + public static class LocalExecHandle extends ExecHandle { protected Process process; protected OutputStream stdin; protected InputStream stdout, stderr; protected ExitMonitorTask task; protected String cmdline; protected Charset charset; + protected StringBuilder output_sb; public LocalExecHandle(Process process, OutputStream stdin, InputStream stdout, InputStream stderr, ExitMonitorTask task, String cmdline) { this.process = process; @@ -202,7 +202,18 @@ public class LocalHost extends Host { this.task = task; this.cmdline = cmdline; } + + @Override + public boolean isRunning() { + try { + process.exitValue(); + return false; + } catch ( Exception ex ) { + return true; + } + } + @Override public void close() { for ( int i=0 ; i < 10 ; i++ ) { try { @@ -213,26 +224,25 @@ public class LocalHost extends Host { } } - public Object[] run_read_streams(Charset charset) throws IOException, InterruptedException { - String o = exec_copy_lines(stdout, charset); - String e = exec_copy_lines(stderr, null); - - int w = process.waitFor(); - - try { - process.destroy(); - } catch ( Exception ex ) { - - } - if (task!=null) - task.cancel(); - - return new Object[]{o, e, w}; + protected void run(Charset charset) throws IOException, InterruptedException { + output_sb = new StringBuilder(1024); + exec_copy_lines(output_sb, stdout, charset); + // + // ignores STDERR + //exec_copy_lines(stderr, null); + + int w = process.waitFor(); + + try { + process.destroy(); + } catch ( Exception ex ) { + + } + if (task!=null) + task.cancel(); } - protected String exec_copy_lines(InputStream in, Charset charset) throws IOException { - StringBuilder sb = new StringBuilder(4096); - + protected void exec_copy_lines(StringBuilder sb, InputStream in, Charset charset) throws IOException { DefaultCharsetDeciderDecoder d = charset == null ? null : PhptTestCase.newCharsetDeciderDecoder(); ByLineReader reader = charset == null ? new NoCharsetByLineReader(in) : new MultiCharsetByLineReader(in, d); String line; @@ -252,8 +262,26 @@ public class LocalHost extends Host { if (reader instanceof AbstractDetectingCharsetReader) this.charset = ((AbstractDetectingCharsetReader)reader).cs;// TODO d.getCommonCharset(); - - return sb.toString(); + } + + @Override + public boolean isCrashed() { + return getExitCode() != 0; + } + + @Override + public String getOutput() { + StringBuilder sb = output_sb; + return sb == null ? "" : sb.toString(); + } + + @Override + public int getExitCode() { + try { + return process.exitValue(); + } catch ( Exception ex ) { + return 0; + } } } // end public static class LocalExecHandle @@ -309,8 +337,8 @@ public class LocalHost extends Host { try { process = builder.start(); } catch ( IOException ex ) { - // randomly sometimes on Linux, get this problem ... try again if (ex.getMessage().contains("file busy")) { + // randomly sometimes on Linux, get this problem ... wait and try again Thread.sleep(100); process = builder.start(); } else { @@ -473,5 +501,33 @@ public class LocalHost extends Host { public String getOSNameLong() { return System.getProperty("os.name"); } + + @Override + public String getAddress() { + try { + Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces(); + Enumeration<InetAddress> addrs; + NetworkInterface ni; + InetAddress addr; + String addr_str; + while (interfaces.hasMoreElements()) { + ni = interfaces.nextElement(); + addrs = ni.getInetAddresses(); + while (addrs.hasMoreElements()) { + addr = addrs.nextElement(); + addr_str = addr.getHostAddress(); + if (addr_str.equals("127.0.0.1")) + continue; + if (addr_str.split("\\.").length==4) + // IPv4 address + return addr_str; + } + } + } catch (SocketException ex) { + ex.printStackTrace(); + } + // no network interfaces + return null; + } // end public String getAddress } // end public class Host diff --git a/src/com/mostc/pftt/host/SSHHost.java b/src/com/mostc/pftt/host/SSHHost.java index 6b9baee..32b9a9b 100644 --- a/src/com/mostc/pftt/host/SSHHost.java +++ b/src/com/mostc/pftt/host/SSHHost.java @@ -189,4 +189,17 @@ public class SSHHost extends RemoteHost { return null; } + @Override + public ExecHandle execThread(String commandline, Map<String, String> env, + String chdir, byte[] stdin_data) throws Exception { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getAddress() { + // TODO Auto-generated method stub + return null; + } + } diff --git a/src/com/mostc/pftt/main/PfttMain.java b/src/com/mostc/pftt/main/PfttMain.java index 2e70c86..2958c68 100644 --- a/src/com/mostc/pftt/main/PfttMain.java +++ b/src/com/mostc/pftt/main/PfttMain.java @@ -129,7 +129,7 @@ public class PfttMain { show_gui("phpt_all", gui); } - test_pack_runner.run_test_list(test_cases); + test_pack_runner.runTestList(test_cases); tmgr.close(); @@ -146,7 +146,7 @@ public class PfttMain { System.err.println(" will generate an FBC report comparing the builds"); System.err.println(); } else { - show_report(new FBCReportGen(base_telem, test_telem)); + // TODO temp show_report(new FBCReportGen(base_telem, test_telem)); } } @@ -175,7 +175,7 @@ public class PfttMain { show_gui("phpt_named", gui); } - test_pack_runner.run_test_list(test_cases); + test_pack_runner.runTestList(test_cases); tmgr.close(); diff --git a/src/com/mostc/pftt/model/phpt/EPhptTestStatus.java b/src/com/mostc/pftt/model/phpt/EPhptTestStatus.java index 6ca1afd..e90514c 100644 --- a/src/com/mostc/pftt/model/phpt/EPhptTestStatus.java +++ b/src/com/mostc/pftt/model/phpt/EPhptTestStatus.java @@ -18,5 +18,5 @@ public enum EPhptTestStatus { /** test is malformed */ BORK, /** an internal PFTT error */ - EXCEPTION + INTERNAL_EXCEPTION } diff --git a/src/com/mostc/pftt/model/phpt/PhpBuild.java b/src/com/mostc/pftt/model/phpt/PhpBuild.java index 6067bb7..d5a2b93 100644 --- a/src/com/mostc/pftt/model/phpt/PhpBuild.java +++ b/src/com/mostc/pftt/model/phpt/PhpBuild.java @@ -109,11 +109,26 @@ public class PhpBuild extends Build { if (ini!=null) return ini; } - ini = new PhpIni(host.getContents(build_path+host.dirSeparator()+"php.ini"), build_path); + String path = getDefaultPhpIniPath(host); + if (host.exists(path)) + ini = new PhpIni(host.getContents(path), build_path); + else + ini = new PhpIni(); + this.php_ini = new WeakReference<PhpIni>(ini); return ini; } + /** calculates path on Host to php.ini used for this build + * + * @param host + * @return + */ + public String getDefaultPhpIniPath(Host host) { + // XXX /etc/cli/php.ini for linux?? + return build_path+host.dirSeparator()+"php.ini"; + } + /** gets the version string for this build * * @param host @@ -166,16 +181,17 @@ public class PhpBuild extends Build { * #isExtensionEnabled checks to see if its enabled. PhpIni#hasExtension will miss builtin/static extensions. * * @param host - * @param ini * @param ext_name * @return * @throws Exception */ - public boolean isExtensionEnabled(Host host, PhpIni ini, String ext_name) throws Exception { + public boolean isExtensionEnabled(Host host, String ext_name) throws Exception { if (ext_name.equals("spl")||ext_name.equals("standard")||ext_name.equals("core")) // these extensions are always there/always builtin return true; + PhpIni ini = getDefaultPhpIni(host); + // key by only extensions, so cache will be reused even if additional directives are added if (ini!=null) ini = ini.getExtensionsOnly(); @@ -241,7 +257,7 @@ public class PhpBuild extends Build { public void setDefaultPhpIni(Host host, PhpIni ini) throws IOException { this.php_ini = new WeakReference<PhpIni>(ini); - host.saveText(build_path+"/php.ini", ini.toString()); + host.saveText(getDefaultPhpIniPath(host), ini.toString()); if (!host.isWindows()) { host.saveText("/etc/php/cli/php.ini", ini.toString()); diff --git a/src/com/mostc/pftt/model/phpt/PhpIni.java b/src/com/mostc/pftt/model/phpt/PhpIni.java index af67ddd..88dbd78 100644 --- a/src/com/mostc/pftt/model/phpt/PhpIni.java +++ b/src/com/mostc/pftt/model/phpt/PhpIni.java @@ -10,6 +10,7 @@ import javax.annotation.Nullable; import com.mostc.pftt.host.Host; import com.mostc.pftt.host.LocalHost; +import com.mostc.pftt.model.sapi.TestCaseGroupKey; import com.mostc.pftt.util.StringUtil; /** A PHP INI is the configuration for PHP. @@ -25,7 +26,7 @@ import com.mostc.pftt.util.StringUtil; * */ -public class PhpIni { +public class PhpIni extends TestCaseGroupKey { // directives public static final String INCLUDE_PATH = "include_path"; public static final String EXTENSION = "extension"; @@ -136,7 +137,7 @@ public class PhpIni { // private HashMap<String, ArrayList<String>> ini_map; private WeakReference<PhpIni> ext_ini; - private WeakReference<String> ini_str; + private WeakReference<String> ini_str, cli_arg; public PhpIni() { ini_map = new HashMap<String, ArrayList<String>>(); @@ -241,7 +242,7 @@ public class PhpIni { public void replaceAll(PhpIni ini) { this.ini_map.putAll(ini.ini_map); if (ini.countDirectives() > 0) - this.ini_str = null; + this.cli_arg = this.ini_str = null; } /** appends all values from all directives from the given PhpIni to this PhpIni @@ -291,7 +292,7 @@ public class PhpIni { ArrayList<String> values = new ArrayList<String>(1); values.add(value); ini_map.put(directive, values); - ini_str = null; + cli_arg = ini_str = null; } public void putMulti(String directive, int value) { @@ -320,7 +321,7 @@ public class PhpIni { public void remove(String directive) { ini_map.remove(directive); - ini_str = null; + cli_arg = ini_str = null; } @Nullable @@ -511,12 +512,19 @@ public class PhpIni { return ext_ini; } - /** + /** generates -d command line arguments to pass these INI directives to php.exe or php-cgi.exe * * @param host * @return */ public String toCliArgString(Host host) { + // + if (cli_arg!=null) { + String cli_arg_str = cli_arg.get(); + if (cli_arg_str!=null) + return cli_arg_str; + } + // StringBuilder sb = new StringBuilder(256); for ( String directive : getDirectives()) { String value = get(directive); @@ -537,8 +545,10 @@ public class PhpIni { sb.append(value); sb.append("\""); } - return sb.toString(); - } + String cli_arg_str = sb.toString(); + cli_arg = new WeakReference<String>(cli_arg_str); + return cli_arg_str; + } // end public String toCliArgString static final Pattern PAT_bs = Pattern.compile("\""); static final Pattern PAT_amp = Pattern.compile("\\&"); static final Pattern PAT_pipe = Pattern.compile("\\|"); diff --git a/src/com/mostc/pftt/model/phpt/PhptTestCase.java b/src/com/mostc/pftt/model/phpt/PhptTestCase.java index 9ed956a..f2a116d 100644 --- a/src/com/mostc/pftt/model/phpt/PhptTestCase.java +++ b/src/com/mostc/pftt/model/phpt/PhptTestCase.java @@ -518,8 +518,11 @@ public class PhptTestCase extends TestCase { /** returns if the string matches the name of this test * * the name should use / only not \\ + * + * test case names are normalized to only use / not \\ * * @param o + * @see #getName * @return */ public boolean isNamed(String o) { diff --git a/src/com/mostc/pftt/model/sapi/ApacheManager.java b/src/com/mostc/pftt/model/sapi/ApacheManager.java new file mode 100644 index 0000000..71a9be3 --- /dev/null +++ b/src/com/mostc/pftt/model/sapi/ApacheManager.java @@ -0,0 +1,34 @@ +package com.mostc.pftt.model.sapi; + +import javax.annotation.concurrent.ThreadSafe; + +import com.mostc.pftt.host.Host; +import com.mostc.pftt.model.phpt.PhpBuild; +import com.mostc.pftt.model.phpt.PhpIni; + +/** manages and monitors Apache HTTPD web server + * + * @author Matt Ficken + * + */ + +@ThreadSafe +public class ApacheManager extends WebServerManager { + + @Override + protected WebServerInstance createWebServerInstance(Host host, PhpBuild build, PhpIni ini, String docroot) { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean allowConcurrentWebServerSAPIInstances() { + return false; + } + + @Override + public boolean isSSLSupported() { + return true; + } + +} diff --git a/src/com/mostc/pftt/model/sapi/BuiltinWebServerInstance.java b/src/com/mostc/pftt/model/sapi/BuiltinWebServerInstance.java new file mode 100644 index 0000000..c447057 --- /dev/null +++ b/src/com/mostc/pftt/model/sapi/BuiltinWebServerInstance.java @@ -0,0 +1,46 @@ +package com.mostc.pftt.model.sapi; + +import com.mostc.pftt.host.Host.ExecHandle; +import com.mostc.pftt.model.phpt.PhpIni; + +/** running instance of PHP's builtin web server. + * + * m + * + * @author Matt Ficken + * + */ + +public class BuiltinWebServerInstance extends WebServerInstance { + protected final int port; + protected final String hostname; + protected final ExecHandle process; + + public BuiltinWebServerInstance(String[] cmd_array, PhpIni ini, ExecHandle process, String hostname, int port) { + super(cmd_array, ini); + this.process = process; + this.hostname = hostname; + this.port = port; + } + + @Override + public String hostname() { + return hostname; + } + + @Override + public int port() { + return port; + } + + @Override + public void close() { + process.close(); + } + + @Override + public boolean isRunning() { + return process.isRunning(); + } + +} // end public class BuiltinWebServerInstance diff --git a/src/com/mostc/pftt/model/sapi/BuiltinWebServerManager.java b/src/com/mostc/pftt/model/sapi/BuiltinWebServerManager.java new file mode 100644 index 0000000..e8b88a2 --- /dev/null +++ b/src/com/mostc/pftt/model/sapi/BuiltinWebServerManager.java @@ -0,0 +1,140 @@ +package com.mostc.pftt.model.sapi; + +import java.io.IOException; +import java.net.Socket; +import java.util.Timer; +import java.util.TimerTask; + +import javax.annotation.concurrent.ThreadSafe; + +import com.mostc.pftt.host.Host; +import com.mostc.pftt.host.LocalHost; +import com.mostc.pftt.model.phpt.PhpBuild; +import com.mostc.pftt.model.phpt.PhpIni; +import com.mostc.pftt.util.ErrorUtil; + +/** manages local instances of PHP's builtin web server + * + * can have multiple concurrent BuiltinWebServerInstances, assigning each one a different + * TCP port number. + * + * @author Matt Ficken + * + */ + +@ThreadSafe +public class BuiltinWebServerManager extends WebServerManager { + protected static final int PORT_RANGE_START = 40000; + // over 49151 may be used by client side of tcp sockets or other dynamic uses + protected static final int PORT_RANGE_STOP = 49151; + // + protected int last_port = PORT_RANGE_START-1; + // + protected final Timer timer; + + public BuiltinWebServerManager() { + timer = new Timer(); + } + + @Override + protected synchronized WebServerInstance createWebServerInstance(Host host, PhpBuild build, PhpIni ini, String docroot) { + String sapi_output = ""; + int port_attempts; + boolean found_port; + for (int total_attempts = 0 ; total_attempts < 3 ; total_attempts++) { + + // find port number not currently in use + port_attempts = 0; + found_port = false; + while (port_attempts < 3) { + last_port++; + if (!isLocalhostTCPPortUsed(last_port)) { + found_port = true; + break; + } else if (last_port > PORT_RANGE_STOP) { + // start over and hope some ports in range are free + last_port = PORT_RANGE_START; + port_attempts++; + } + } + + if (!found_port) { + // try again + sapi_output += "PFTT: Couldn't find unused local port\n"; + continue; + } + + // Windows BN: php -S won't accept connections if it listens on localhost|127.0.0.1 on Windows + // php won't complain about this though, it will run, but be inaccessible + String hostname = host.getLocalhostServableAddress(); + + Host.ExecHandle handle = null; + try { + // run `php.exe -S localhost:NNNN` in docroot + String cmd = build.getPhpExe()+" -S "+hostname+":"+last_port+" "+(ini==null?"":ini.toCliArgString(host)); + + handle = host.execThread(cmd, docroot); + + final Host.ExecHandle handlef = handle; + new Exception().printStackTrace(); // TODO temp + + // ensure server can be connected to + Socket sock = new Socket(hostname, last_port); + if (!sock.isConnected()) + // kill server and try again + throw new IOException("socket not connected"); + sock.close(); + // + + // provide a BuiltinWebServerInstance to handle the running web server instance + final BuiltinWebServerInstance web = new BuiltinWebServerInstance(LocalHost.splitCmdString(cmd), ini, handle, hostname, last_port); + + // check web server periodically to see if it has crashed + timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + if (!handlef.isRunning()) { + try { + if (handlef.isCrashed()) + // notify of web server crash + // + // provide output and exit code + web.notifyCrash(handlef.getOutput(), handlef.getExitCode()); + } finally { + // don't need to check any more + cancel(); + } + } + } // end public void run + }, 0, 500); + // + + return web; + } catch ( Exception ex ) { + if (handle!=null) + // make sure process is killed in this case + handle.close(); + + sapi_output += ErrorUtil.toString(ex) + "\n"; + } + } // end for + + // fallback + sapi_output += "PFTT: wasn't able to start web server instance (after many attempts)... giving up.\n"; + + // return this failure message to client code + return new CrashedWebServerInstance(ini, sapi_output); + } // end protected synchronized WebServerInstance createWebServerInstance + + @Override + public boolean allowConcurrentWebServerSAPIInstances() { + return true; + } + + @Override + public boolean isSSLSupported() { + // XXX can this web server support SSL? + return false; + } + +} // end public class BuiltinWebServerManager diff --git a/src/com/mostc/pftt/model/sapi/CrashedWebServerInstance.java b/src/com/mostc/pftt/model/sapi/CrashedWebServerInstance.java new file mode 100644 index 0000000..154ea27 --- /dev/null +++ b/src/com/mostc/pftt/model/sapi/CrashedWebServerInstance.java @@ -0,0 +1,46 @@ +package com.mostc.pftt.model.sapi; + +import com.mostc.pftt.model.phpt.PhpIni; + +/** special marker for WebServerInstances that crashed on startup/couldn't be started. + * + * special marker to just pass the crash output of the web server + * + * @see #getSAPIOutput + * @author Matt Ficken + * + */ + +public class CrashedWebServerInstance extends WebServerInstance { + protected final String sapi_output; + + public CrashedWebServerInstance(PhpIni ini, String sapi_output) { + super(null, ini); + this.sapi_output = sapi_output; + } + + @Override + public String getSAPIOutput() { + return sapi_output; + } + + @Override + public String hostname() { + return null; + } + + @Override + public int port() { + return 0; + } + + @Override + public void close() { + } + + @Override + public boolean isRunning() { + return false; + } + +} diff --git a/src/com/mostc/pftt/model/sapi/IISManager.java b/src/com/mostc/pftt/model/sapi/IISManager.java new file mode 100644 index 0000000..992d8c6 --- /dev/null +++ b/src/com/mostc/pftt/model/sapi/IISManager.java @@ -0,0 +1,34 @@ +package com.mostc.pftt.model.sapi; + +import javax.annotation.concurrent.ThreadSafe; + +import com.mostc.pftt.host.Host; +import com.mostc.pftt.model.phpt.PhpBuild; +import com.mostc.pftt.model.phpt.PhpIni; + +/** manages and monitors IIS and IIS express web servers + * + * @author Matt Ficken + * + */ + +@ThreadSafe +public class IISManager extends WebServerManager { + + @Override + protected WebServerInstance createWebServerInstance(Host host, PhpBuild build, PhpIni ini, String docroot) { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean allowConcurrentWebServerSAPIInstances() { + return false; + } + + @Override + public boolean isSSLSupported() { + return true; + } + +} diff --git a/src/com/mostc/pftt/model/sapi/SAPIInstance.java b/src/com/mostc/pftt/model/sapi/SAPIInstance.java new file mode 100644 index 0000000..06f4d5c --- /dev/null +++ b/src/com/mostc/pftt/model/sapi/SAPIInstance.java @@ -0,0 +1,13 @@ +package com.mostc.pftt.model.sapi; + +/** Running instance of a SAPI like a web server + * + * @author Matt Ficken + * + */ + +public abstract class SAPIInstance extends TestCaseGroupKey { + public abstract String getSAPIOutput(); + public abstract void close(); + public abstract boolean isRunning(); +} diff --git a/src/com/mostc/pftt/model/sapi/SAPIManager.java b/src/com/mostc/pftt/model/sapi/SAPIManager.java new file mode 100644 index 0000000..87a8ac6 --- /dev/null +++ b/src/com/mostc/pftt/model/sapi/SAPIManager.java @@ -0,0 +1,8 @@ +package com.mostc.pftt.model.sapi; + +import javax.annotation.concurrent.ThreadSafe; + +@ThreadSafe +public abstract class SAPIManager { + +} diff --git a/src/com/mostc/pftt/model/sapi/TestCaseGroupKey.java b/src/com/mostc/pftt/model/sapi/TestCaseGroupKey.java new file mode 100644 index 0000000..af7f018 --- /dev/null +++ b/src/com/mostc/pftt/model/sapi/TestCaseGroupKey.java @@ -0,0 +1,6 @@ +package com.mostc.pftt.model.sapi; + +public abstract class TestCaseGroupKey { + public abstract boolean equals(Object o); + public abstract int hashCode(); +} diff --git a/src/com/mostc/pftt/model/sapi/WebServerInstance.java b/src/com/mostc/pftt/model/sapi/WebServerInstance.java new file mode 100644 index 0000000..d1ee66f --- /dev/null +++ b/src/com/mostc/pftt/model/sapi/WebServerInstance.java @@ -0,0 +1,154 @@ +package com.mostc.pftt.model.sapi; + +import java.util.LinkedList; + +import javax.annotation.concurrent.ThreadSafe; + +import com.mostc.pftt.model.phpt.PhpIni; +import com.mostc.pftt.model.phpt.PhptTestCase; +import com.mostc.pftt.util.StringUtil; + +/** an instance of a web server + * + * @see WebServerManager + * @author Matt Ficken + * + */ + +@ThreadSafe +public abstract class WebServerInstance extends SAPIInstance { + protected LinkedList<PhptTestCase> active_test_cases; + private boolean crashed = false; + private String sapi_output = ""; + private Object sync_lock = new Object(); + protected final PhpIni ini; + protected final String[] cmd_array; + WebServerInstance replacement; // @see WebServerManager#getWebServerInstance + + public WebServerInstance(String[] cmd_array, PhpIni ini) { + this.cmd_array = cmd_array; + this.ini = ini; + active_test_cases = new LinkedList<PhptTestCase>(); + } + + @Override + public int hashCode() { + return this.port() + StringUtil.hashCode(this.hostname()); + } + + @Override + public boolean equals(Object o) { + if (o==this) + return true; + if (o instanceof WebServerInstance) { + WebServerInstance wsio = (WebServerInstance) o; + return wsio.port() == this.port() && StringUtil.equalsIC(wsio.hostname(), this.hostname()); + } + return false; + } + + @Override + public String getSAPIOutput() { + synchronized(sync_lock) { + return sapi_output; + } + } + + /** handles reporting an instance that crashed + * + * @param output - any output server returned when it crashed (or null) + * @param exit_code - exit code that was returned + */ + public void notifyCrash(String output, int exit_code) { + synchronized(sync_lock) { + // + if (crashed) { + if (StringUtil.isNotEmpty(output)) { + // shouldn't happen, but capture this output if it does + StringBuilder sb = new StringBuilder(); + if (sapi_output!=null) + sb.append(sapi_output); + sb.append("PFTT: later web server returned exit code("+exit_code+") and output:\n"); + sb.append(output); + sapi_output = sb.toString(); + } + return; + } + // + + // if crash, record output with all tests that were running during crash + crashed = true; + + StringBuilder sb = new StringBuilder(1024); + + sb.append("PFTT: web server crashed with exit code: "+exit_code); + synchronized(active_test_cases) { + sb.append("PFTT: while running these tests("+active_test_cases.size()+"):\n"); + for (PhptTestCase test_case : active_test_cases ) { + sb.append("PFTT: "); + sb.append(test_case.getName()); + sb.append('\n'); + } + } + if (StringUtil.isEmpty(output)) { + sb.append("PFTT: web server returned no output when it exited.\n"); + } else { + sb.append("PFTT: before crashing/exiting web server returned("+output.length()+"):\n"); + sb.append(output); + } + + sapi_output = sb.toString(); + } // end sync + } // end protected void notifyCrash + + /** called before HTTP request made to server for given test_case + * + * @param test_case + */ + public void notifyTestPreRequest(PhptTestCase test_case) { + synchronized(active_test_cases) { + active_test_cases.add(test_case); + } + } + + /** called immediately after HTTP response from server for the test case. + * + * the sooner this is called the better, to minimize getting other test cases that were + * executed after this one (which wouldn't be related to a crash this test case caused). + * + * @param test_case + */ + public void notifyTestPostResponse(PhptTestCase test_case) { + synchronized(active_test_cases) { + active_test_cases.remove(test_case); + } + if (replacement!=null) + replacement.notifyTestPostResponse(test_case); + } + + /** returns TRUE if this web server crashed + * + * @return + */ + public boolean isCrashed() { + synchronized(sync_lock) { + return crashed; + } + } + + public abstract String hostname(); + public abstract int port(); + + /** PhpIni set for this web server + * + * @return + */ + public PhpIni getPhpIni() { + return ini; + } + + public String[] getCmdString() { + return cmd_array; + } + +} // end public abstract class WebServerInstance diff --git a/src/com/mostc/pftt/model/sapi/WebServerManager.java b/src/com/mostc/pftt/model/sapi/WebServerManager.java new file mode 100644 index 0000000..195c57c --- /dev/null +++ b/src/com/mostc/pftt/model/sapi/WebServerManager.java @@ -0,0 +1,118 @@ +package com.mostc.pftt.model.sapi; + +import java.io.IOException; +import java.net.Socket; +import java.util.Iterator; +import java.util.LinkedList; + +import javax.annotation.concurrent.ThreadSafe; + +import com.mostc.pftt.host.Host; +import com.mostc.pftt.model.phpt.PhpBuild; +import com.mostc.pftt.model.phpt.PhpIni; + +/** Manages a certain type of web server, such as PHP's builtin web server. + * + * @author Matt Ficken + * + */ + +@ThreadSafe +public abstract class WebServerManager extends SAPIManager { + public static final String LOCALHOST = "localhost"; + // + protected final LinkedList<WebServerInstance> instances; + + public WebServerManager() { + instances = new LinkedList<WebServerInstance>(); + } + + /** closes all web servers this managed + * + */ + public void close() { + synchronized(instances) { + Iterator<WebServerInstance> it = instances.iterator(); + while (it.hasNext()) { + it.next().close(); + it.remove(); + } + } + } + + /** gets a running WebServerInstance + * + * if given an existing assigned WebServerInstance that hasn't crashed and is running, + * returns that WebServerInstance for additional use. + * + * otherwise, if no WebServerInstance or if it crashed, creates a new WebServerInstance and returns it + * + * if the same crashed WebServerInstance is provided to this several times, doesn't create replacement for each, + * uses the first replacement (to not have crashed). + * + * @param host + * @param build + * @param ini + * @param docroot + * @param assigned + * @return + */ + public WebServerInstance getWebServerInstance(Host host, PhpBuild build, PhpIni ini, String docroot, WebServerInstance assigned) { + WebServerInstance sapi; + if (assigned!=null) { + if (!assigned.isCrashed()) + return assigned; + synchronized(assigned) { + for (WebServerInstance c=assigned.replacement ; c != null ; c = c.replacement) { + synchronized(c) { + if (!c.isCrashed()) + return c; + } + } + } + assigned.replacement = sapi = createWebServerInstance(host, build, ini, docroot); + synchronized(assigned.active_test_cases) { + sapi.active_test_cases.addAll(assigned.active_test_cases); + } + } else { + sapi = createWebServerInstance(host, build, ini, docroot); + } + if (!sapi.isCrashed()) { + synchronized(instances) { + instances.add(sapi); + } + } + return sapi; + } + + protected abstract WebServerInstance createWebServerInstance(Host host, PhpBuild build, PhpIni ini, String docroot); + + /** some web servers can only have one active instance at any one time + * + * @see WebServerInstance#close + * @see WebServerInstance#isRunning + * @return + */ + public abstract boolean allowConcurrentWebServerSAPIInstances(); + + public abstract boolean isSSLSupported(); + + /** checks if the given port is being used on localhost + * + * @param port + */ + public static final boolean isLocalhostTCPPortUsed(int port) { + return isTCPPortUsed(LOCALHOST, port); + } + + public static final boolean isTCPPortUsed(String hostname, int port) { + try { + Socket sock = new Socket(hostname, port); + + return sock.isConnected(); + } catch ( IOException ex ) { + return false; + } + } + +} // end public abstract class WebServerManager diff --git a/src/com/mostc/pftt/model/sapi/package.html b/src/com/mostc/pftt/model/sapi/package.html new file mode 100644 index 0000000..2b4b5fd --- /dev/null +++ b/src/com/mostc/pftt/model/sapi/package.html @@ -0,0 +1,7 @@ +<html> + <body> + <p> + Model and management of PHP SAPIs (CLI and web servers) + </p> + </body> +</html> \ No newline at end of file diff --git a/src/com/mostc/pftt/runner/AbstractPhptTestCaseRunner.java b/src/com/mostc/pftt/runner/AbstractPhptTestCaseRunner.java index 2d3bf13..cd55764 100644 --- a/src/com/mostc/pftt/runner/AbstractPhptTestCaseRunner.java +++ b/src/com/mostc/pftt/runner/AbstractPhptTestCaseRunner.java @@ -2,6 +2,15 @@ package com.mostc.pftt.runner; import java.io.IOException; +import com.mostc.pftt.host.Host; +import com.mostc.pftt.model.phpt.EPhptTestStatus; +import com.mostc.pftt.model.phpt.PhpBuild; +import com.mostc.pftt.model.phpt.PhpIni; +import com.mostc.pftt.model.phpt.PhptTestPack; +import com.mostc.pftt.model.phpt.PhptTestCase; +import com.mostc.pftt.telemetry.PhptTelemetryWriter; +import com.mostc.pftt.telemetry.PhptTestResult; + public abstract class AbstractPhptTestCaseRunner { public static final String ENV_SCRIPT_FILENAME = "SCRIPT_FILENAME"; public static final String ENV_PATH_TRANSLATED = "PATH_TRANSLATED"; @@ -17,4 +26,43 @@ public abstract class AbstractPhptTestCaseRunner { public static final String ENV_HTTP_CONTENT_ENCODING = "HTTP_CONTENT_ENCODING"; public abstract void runTest() throws IOException, Exception, Throwable; + + public static PhpIni createIniForTest(Host host, PhpBuild build, PhptTestPack test_pack, PhptTestCase test_case) { + PhpIni ini = PhpIni.createDefaultIniCopy(host); + ini.replaceAll(test_case.getINI(test_pack, host)); + ini.addToIncludePath(host, test_pack.getTestPack()); + return ini; + } + + /** TODO + * + * @param twriter + * @param host + * @param build + * @param test_case + * @return + * @throws Exception + */ + public static boolean willSkip(PhptTelemetryWriter twriter, Host host, PhpBuild build, PhptTestCase test_case) throws Exception { + if (!host.isWindows() && test_case.getName().contains("-win32")) { + // skip windows specific tests if host is not windows + + twriter.addResult(new PhptTestResult(host, EPhptTestStatus.XSKIP, test_case, "OS not supported", null, null, null, null, null, null, null, null, null, null)); + + return true; + } else if (test_case.isNamed("ext/standard/tests/php_ini_loaded_file.phpt")||test_case.isNamed("tests/run-test/test010.phpt")||test_case.isNamed("ext/standard/tests/misc/time_sleep_until_basic.phpt") || test_case.getName().contains("session") || test_case.isNamed("ext/standard/tests/misc/time_nanosleep_basic.phpt")) { + twriter.addResult(new PhptTestResult(host, EPhptTestStatus.XSKIP, test_case, "test sometimes randomly fails, ignore it", null, null, null, null, null, null, null, null, null, null)); + + return true; + } else if (test_case.isExtensionTest() && !build.isExtensionEnabled(host, test_case.getExtensionName())) { + // if extension-under-test is not loaded, don't bother running test since it'll just be skipped (or false fail) + + twriter.addResult(new PhptTestResult(host, EPhptTestStatus.SKIP, test_case, "Extension not loaded", null, null, null, null, null, null, null, null, null, null)); + + return true; + } + + + return false; + } // end public static boolean willSkip } diff --git a/src/com/mostc/pftt/runner/AbstractPhptTestCaseRunner2.java b/src/com/mostc/pftt/runner/AbstractPhptTestCaseRunner2.java index df8f725..df4e987 100644 --- a/src/com/mostc/pftt/runner/AbstractPhptTestCaseRunner2.java +++ b/src/com/mostc/pftt/runner/AbstractPhptTestCaseRunner2.java @@ -2,11 +2,8 @@ package com.mostc.pftt.runner; import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileOutputStream; -import java.io.FileWriter; import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; +import java.nio.charset.Charset; import java.util.HashMap; import java.util.regex.Pattern; import java.util.zip.Deflater; @@ -14,9 +11,7 @@ import java.util.zip.DeflaterOutputStream; import org.incava.util.diff.Diff; -import com.mostc.pftt.host.ExecOutput; import com.mostc.pftt.host.Host; -import com.mostc.pftt.host.LocalHost; import com.mostc.pftt.model.phpt.EPhptSection; import com.mostc.pftt.model.phpt.EPhptTestStatus; import com.mostc.pftt.model.phpt.PhpBuild; @@ -36,7 +31,8 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu protected final Host host; protected final PhpBuild build; protected final PhptTestPack test_pack; - protected final PhptTestCase test_case; + protected final PhptTestCase test_case; + // TODO shift all ENV handling to CliPhptTestCaseRunner protected HashMap<String,String> env; protected final ScenarioSet scenario_set; @@ -52,53 +48,46 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu protected String temp_dir, test_dir; protected String main_file_name; protected String skip_cmd; - protected ExecOutput output; protected String test_file; protected String test_skipif; protected String test_clean; - protected String tmp_post; - protected PhpIni ini; - + protected String tmp_post, content_type; + protected final PhpIni ini; + + /** runs the test case and reports the results to the PhptTelemetryManager + * + * @see #willSkip - called by PhptTestPackRunner before #runTest is called + * + */ @Override public void runTest() throws IOException, Exception, Throwable { - // Default ini settings - ini = PhpIni.createDefaultIniCopy(host); +// ini = PhpIni.createDefaultIniCopy(host); if (!prepare()) return; - - if (!host.isWindows() && test_case.getName().contains("-win32")) { - // skip windows specific tests if host is not windows - - twriter.addResult(new PhptTestResult(host, EPhptTestStatus.XSKIP, test_case, "OS not supported", null, null, null, null, null, null, null, null, null, null)); - - return; - } else if (test_case.isNamed("ext/standard/tests/php_ini_loaded_file.phpt")||test_case.isNamed("tests/run-test/test010.phpt")||test_case.isNamed("ext/standard/tests/misc/time_sleep_until_basic.phpt") || test_case.getName().contains("session") || test_case.isNamed("ext/standard/tests/misc/time_nanosleep_basic.phpt")) { - twriter.addResult(new PhptTestResult(host, EPhptTestStatus.XSKIP, test_case, "test sometimes randomly fails, ignore it", null, null, null, null, null, null, null, null, null, null)); - - return; - } - // #prepare() loads --INI-- section from PHPT - if (test_case.isExtensionTest() && !build.isExtensionEnabled(host, ini, test_case.getExtensionName())) { - // if extension-under-test is not loaded, don't bother running test since it'll just be skipped (or false fail) - - twriter.addResult(new PhptTestResult(host, EPhptTestStatus.SKIP, test_case, "Extension not loaded", null, null, null, null, null, null, null, null, null, null)); - - return; - } - - this.executeSkipIf(); // XXX check if prepare() has borked the test -> don't bother running SKIPIF leads to test_skipif=null -> exception - if (!evalSkipIf()) { - this.prepareTest(); - this.executeTest(); - this.evalTest(); - this.executeClean(); + + notifyStart(); + + try { + if (!evalSkipIf(executeSkipIf())) { + prepareTest(); + String test_output = executeTest(); + notifyEnd(); // do ASAP + evalTest(test_output, null); // TODO null + executeClean(); + } + } finally { + // ok to call twice + notifyEnd(); } } + protected void notifyStart() {} + protected void notifyEnd() {} - public AbstractPhptTestCaseRunner2(PhptThread thread, PhptTestCase test_case, PhptTelemetryWriter twriter, Host host, ScenarioSet scenario_set, PhpBuild build, PhptTestPack test_pack) { + public AbstractPhptTestCaseRunner2(PhpIni ini, PhptThread thread, PhptTestCase test_case, PhptTelemetryWriter twriter, Host host, ScenarioSet scenario_set, PhpBuild build, PhptTestPack test_pack) { + this.ini = ini; this.thread = thread; this.test_case = test_case; this.twriter = twriter; @@ -110,9 +99,14 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu env = new HashMap<String,String>(); } - public boolean prepare() throws IOException, Exception { - //tested_file = test_case.name; - + /** prepares to execute the test case up to executing the SKIPIF section + * + * @see #prepareTest + * @return FALSE - if preparation fails so test can't be executed + * @throws IOException + * @throws Exception + */ + protected boolean prepare() throws IOException, Exception { if (test_case.hasBorkInfo()) { twriter.addResult(new PhptTestResult(host, EPhptTestStatus.BORK, test_case, test_case.getBorkInfo(), null, null, null, null, null, null, null, null, null, null)); @@ -125,11 +119,10 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu return false; } - //tested = test_case.getTrim(EPHPTSection.TEST); - selected_php_exe = build.getPhpExe(); /* For GET/POST tests, check if cgi sapi is available and if it is, use it. */ + // TODO php-cgi not required for testing with web server - can't xskip if (test_case.containsAnySection(EPhptSection.REQUEST, EPhptSection.GET, EPhptSection.POST, EPhptSection.PUT, EPhptSection.POST_RAW, EPhptSection.COOKIE, EPhptSection.EXPECTHEADERS)) { if (build.hasPhpCgiExe()) { selected_php_exe = build.getPhpCgiExe() + " -C "; @@ -185,13 +178,11 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu // Any special ini settings // these may overwrite the test defaults... - if (test_case.containsSection(EPhptSection.INI)) { - ini.replaceAll(test_case.getINI(test_pack, host)); - } - - // ask the scenario to prepare for the test (configure for database, file server, etc...) +// if (test_case.containsSection(EPhptSection.INI)) { +// ini.replaceAll(test_case.getINI(test_pack, host)); +// } + // TODO scenario.prepare(null, test_file, env, ini_map); - // TODO scenario_set.prepare(); // read ENV vars from test, from its parent (if a test redirected to this test), and merge from scenario env = test_case.getENV(env, host, build); @@ -201,11 +192,6 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu if (build.hasPhpCgiExe()) env.put(ENV_TEST_PHP_CGI_EXECUTABLE, build.getPhpCgiExe()); - - // setup the include path in the INI Configuration - ini.addToIncludePath(host, Host.dirname(test_case.getName())); - ini.addToIncludePath(host, test_pack.getTestPack()); // TODO ? - // ini_settings = ini.toCliArgString(host); @@ -213,14 +199,26 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu } // end boolean prepare static final Pattern PAT_bs = Pattern.compile("\""); - public abstract void executeSkipIf() throws Exception; - - public boolean evalSkipIf() throws IOException { + /** executes SKIPIF section and returns output + * + * @return + * @throws Exception + */ + protected abstract String executeSkipIf() throws Exception; + + /** evaluates the execution output of the SKIPIF section to decide if test should be + * skipped. reports result to PhptTelemetryManager. + * + * @param output + * @return FALSE - skip test + * @throws IOException + */ + public boolean evalSkipIf(String output) throws IOException { if (skip_cmd==null) // execute this test, don't skip it return false; - String lc_output = output.output.toLowerCase(); + String lc_output = output.toLowerCase(); if (lc_output.contains("skip")) { // test is to be skipped @@ -228,15 +226,15 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu if (host.isWindows()) { if ( (lc_output.contains("only")&&lc_output.contains("linux"))||(lc_output.contains("not")&&lc_output.contains("windows"))) // can"t run this test on this OS - twriter.addResult(new PhptTestResult(host, EPhptTestStatus.XSKIP, test_case, output.output, null, null, null, null, null, null, null, null, null, null)); + twriter.addResult(new PhptTestResult(host, EPhptTestStatus.XSKIP, test_case, output, null, null, null, null, null, null, null, null, null, null)); else - twriter.addResult(new PhptTestResult(host, EPhptTestStatus.SKIP, test_case, output.output, null, null, null, null, null, null, null, null, null, null)); + twriter.addResult(new PhptTestResult(host, EPhptTestStatus.SKIP, test_case, output, null, null, null, null, null, null, null, null, null, null)); } else { if ( (lc_output.contains("only")&&lc_output.contains("windows"))||(lc_output.contains("not")&&lc_output.contains("linux"))) // can"t run this test on this OS - twriter.addResult(new PhptTestResult(host, EPhptTestStatus.XSKIP, test_case, output.output, null, null, null, null, null, null, null, null, null, null)); + twriter.addResult(new PhptTestResult(host, EPhptTestStatus.XSKIP, test_case, output, null, null, null, null, null, null, null, null, null, null)); else - twriter.addResult(new PhptTestResult(host, EPhptTestStatus.SKIP, test_case, output.output, null, null, null, null, null, null, null, null, null, null)); + twriter.addResult(new PhptTestResult(host, EPhptTestStatus.SKIP, test_case, output, null, null, null, null, null, null, null, null, null, null)); } // skip this test @@ -248,7 +246,13 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu } // end void evalSkipIf static final Pattern PATTERN_CONTENT_TYPE = Pattern.compile("Content-Type:(.*)"); - public void prepareTest() throws Exception { + /** prepares to execute the test after the SKIPIF section is executed (if any) + * #prepare prepares only up to that, this does the rest. + * to avoid doing a full preparation for tests that will just be skipped. + * + * @throws Exception + */ + protected void prepareTest() throws Exception { if (test_case.containsSection(EPhptSection.FILE_EXTERNAL)) { // open external file and copy to test_file (binary, no char conversion - which could break it - often this is a PHAR file - which will be broken if charset coversion is done) host.copyFile(test_pack.getTestPack()+host.dirSeparator()+Host.dirname(test_case.getName()) + "/" + test_case.get(EPhptSection.FILE_EXTERNAL), test_file); @@ -297,16 +301,16 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu // // content type should look like this (boundary may differ): // "multipart/form-data; boundary=---------------------------20896060251896012921717172737" - String ct = line.substring("Content-Type: ".length()); + content_type = line.substring("Content-Type: ".length()); - env.put(ENV_CONTENT_TYPE, ct); + env.put(ENV_CONTENT_TYPE, content_type); first_ct = false; } else if (first_ct) { // content type may look like this: // "multipart/form-data" or "application/x-www-urlencoded" - String ct = line.substring("Content-Type: ".length()); + content_type = line.substring("Content-Type: ".length()); - env.put(ENV_CONTENT_TYPE, ct); + env.put(ENV_CONTENT_TYPE, content_type); first_ct = false; } else { request_sb.append(line); @@ -349,7 +353,7 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu if (StringUtil.isEmpty(env.get(ENV_CONTENT_TYPE))) { String[] res = StringUtil.getMatches(PATTERN_CONTENT_TYPE, line, twriter); if (StringUtil.isNotEmpty(res)) { - env.put(ENV_CONTENT_TYPE, res[1].trim()); + env.put(ENV_CONTENT_TYPE, content_type = res[1].trim()); continue; } } @@ -404,7 +408,7 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu env.put(ENV_REQUEST_METHOD, "POST"); if (!env.containsKey(ENV_CONTENT_TYPE)) - env.put(ENV_CONTENT_TYPE, "application/x-www-form-urlencoded"); + env.put(ENV_CONTENT_TYPE, content_type = "application/x-www-form-urlencoded"); // critical: php-cgi won"t read more bytes than this (thus some input can go missing) env.put(ENV_CONTENT_LENGTH, ""+content_length); @@ -424,65 +428,9 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu env.put(ENV_USE_ZEND_ALLOC, "1"); // prepare STDIN - String stdin_file = test_file + ".stdin"; - if (stdin_file.startsWith(test_pack.getTestPack())) { - stdin_file = stdin_file.substring(test_pack.getTestPack().length()); - if (stdin_file.startsWith("/")||stdin_file.startsWith("\\")) - stdin_file = stdin_file.substring(1); - stdin_file = twriter.telem_dir+"/"+stdin_file; - } - new File(stdin_file).getParentFile().mkdirs(); - if (stdin_post!=null) { - FileOutputStream fw = new FileOutputStream(stdin_file); - fw.write(stdin_post); - fw.close(); - } + prepareSTDIN(); - // useful: rm -rf `find ./ -name "*.sh"` - // - // create a .cmd (Windows batch script) or .sh (shell script) that will actually execute PHP - // this enables PHP to be executed like what PFTT does, but using a shell|batch script - shell_file = test_file + (host.isWindows() ? ".cmd" : ".sh" ); - if (shell_file.startsWith(test_pack.getTestPack())) { - shell_file = shell_file.substring(test_pack.getTestPack().length()); - if (shell_file.startsWith("/")||shell_file.startsWith("\\")) - shell_file = shell_file.substring(1); - shell_file = twriter.telem_dir+"/"+shell_file; - } - StringWriter sw = new StringWriter(); - PrintWriter fw = new PrintWriter(sw); - if (host.isWindows()) { - fw.println("@echo off"); // critical: or output will include these commands and fail test - } else { - fw.println("#!/bin/sh"); - } - fw.println("cd "+host.fixPath(test_dir)); - for ( String name : env.keySet()) { - String value = env.get(name); - if (value==null) - value = ""; - else - value = StringUtil.replaceAll(PAT_bs, "\\\\\"", StringUtil.replaceAll(PAT_dollar, "\\\\\\$", value)); - - if (host.isWindows()) { - if (value.contains(" ")) - // important: on Windows, don't use " " for script variables unless you have to - // the " " get passed into PHP so variables won't be read correctly - fw.println("set "+name+"=\""+value+"\""); - else - fw.println("set "+name+"="+value+""); - } else - fw.println("export "+name+"=\""+value+"\""); - } - fw.println(cmd); - fw.close(); - shell_script = sw.toString(); - FileWriter w = new FileWriter(shell_file); - fw = new PrintWriter(w); - fw.print(shell_script); - fw.flush(); - fw.close(); - w.close(); + createShellScript(); Thread.yield(); if (!host.isWindows()) { @@ -494,21 +442,48 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu } // end void prepareTest static final Pattern PAT_dollar = Pattern.compile("\\$"); - public abstract void executeTest() throws Exception; - - public abstract void executeClean() throws Exception; + protected abstract void createShellScript() throws IOException; + protected abstract void prepareSTDIN() throws IOException; + + /** executes the test (the TEST section of PhptTestCase) and returns the actual output + * + * @return + * @throws Exception + */ + protected abstract String executeTest() throws Exception; + + /** executes CLEAN section of test to cleanup after execution + * + * @throws Exception + */ + protected abstract void executeClean() throws Exception; + + /** returns output from SAPI (ex: Web Server) used to run the test, + * if it crashed. if SAPI did not crash, returns null. + * + * used to record crash output of a web server along with test result for + * later analysis. + * + * @see WebserverInstance#getSAPIOutput + * @return + */ + protected abstract String getCrashedSAPIOutput(); private boolean check() { return !test_case.isNamed("ext/phar/tests/tar/phar_setsignaturealgo2.phpt"); } - public void evalTest() throws Throwable { - if (output.exit_code!=0 && output.output.trim().length()==0) { - output.output = "PFTT: error: maybe crash?: php.exe exited with non-zero code: " + output.exit_code; - } + /** evaluates the output of the executed test and reports the result + * + * @param output + * @param charset + * @throws Throwable + */ + public void evalTest(String output, Charset charset) throws Throwable { + String output_trim = output.trim(); // line endings are already made consistent by Host#exec - String expected, output_trim, preoverride_actual = null; + String expected, preoverride_actual = null; if (test_case.containsSection(EPhptSection.EXPECTF) || test_case.containsSection(EPhptSection.EXPECTREGEX)) { @@ -520,8 +495,8 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu boolean expected_re_match; - output.output = remove_header_from_output(output.output); - output_trim = output.output.trim(); + output = remove_header_from_output(output); + output_trim = output.trim(); try { expected_re_match = test_case.getExpectedCompiled(host, twriter).match(output_trim); @@ -531,19 +506,19 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu } if (expected_re_match||check()) { - twriter.addResult(new PhptTestResult(host, test_case.isXFail()?EPhptTestStatus.XFAIL:EPhptTestStatus.PASS, test_case, output.output, null, null, output.charset, env, LocalHost.splitCmdString(cmd), stdin_post, shell_script, null, null, null)); + twriter.addResult(new PhptTestResult(host, test_case.isXFail()?EPhptTestStatus.XFAIL:EPhptTestStatus.PASS, test_case, output, null, null, charset, env, splitCmdString(), stdin_post, shell_script, null, null, null)); return; } } else if (test_case.containsSection(EPhptSection.EXPECT)) { expected = test_case.getTrim(EPhptSection.EXPECT); - output.output = remove_header_from_output(output.output); - output_trim = output.output.trim(); + output = remove_header_from_output(output); + output_trim = output.trim(); if (output_trim.equals(expected)||output_trim.contains(expected)||expected.contains(output_trim)||check()) { - twriter.addResult(new PhptTestResult(host, test_case.isXFail()?EPhptTestStatus.XFAIL:EPhptTestStatus.PASS, test_case, output.output, null, null, output.charset, env, LocalHost.splitCmdString(cmd), stdin_post, shell_script, null, null, null)); + twriter.addResult(new PhptTestResult(host, test_case.isXFail()?EPhptTestStatus.XFAIL:EPhptTestStatus.PASS, test_case, output, null, null, charset, env, splitCmdString(), stdin_post, shell_script, null, null, null)); return; } @@ -555,17 +530,17 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu // compare again if (output_trim.equals(expected)||output_trim.contains(expected)||expected.contains(output_trim)||(output_trim.length()>20&&expected.length()>20&&output_trim.substring(10, 20).equals(expected.substring(10, 20)))) { - twriter.addResult(new PhptTestResult(host, test_case.isXFail()?EPhptTestStatus.XFAIL:EPhptTestStatus.PASS, test_case, output.output, null, null, output.charset, env, LocalHost.splitCmdString(cmd), stdin_post, shell_script, null, null, null)); + twriter.addResult(new PhptTestResult(host, test_case.isXFail()?EPhptTestStatus.XFAIL:EPhptTestStatus.PASS, test_case, output, null, null, charset, env, splitCmdString(), stdin_post, shell_script, null, null, null)); return; } // end if } } else if (test_case.containsSection(EPhptSection.EXPECTHEADERS)) { - output.output = remove_header_from_output(output.output); - output_trim = output.output.trim(); + output = remove_header_from_output(output); + output_trim = output.trim(); if (StringUtil.isEmpty(output_trim)) { - twriter.addResult(new PhptTestResult(host, test_case.isXFail()?EPhptTestStatus.XFAIL:EPhptTestStatus.PASS, test_case, output.output, null, null, output.charset, env, LocalHost.splitCmdString(cmd), stdin_post, shell_script, null, null, null)); + twriter.addResult(new PhptTestResult(host, test_case.isXFail()?EPhptTestStatus.XFAIL:EPhptTestStatus.PASS, test_case, output, null, null, charset, env, splitCmdString(), stdin_post, shell_script, null, null, null)); return; } @@ -574,12 +549,12 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu // if here, test failed! if (test_case.isXFail()) { - twriter.addResult(new PhptTestResult(host, EPhptTestStatus.XFAIL, test_case, output.output, null, null, output.charset, env, LocalHost.splitCmdString(cmd), stdin_post, shell_script, null, null, preoverride_actual)); + twriter.addResult(new PhptTestResult(host, EPhptTestStatus.XFAIL, test_case, output, null, null, charset, env, splitCmdString(), stdin_post, shell_script, null, null, preoverride_actual)); } else { // test is FAIL // generate a diff - String[] actual_lines = StringUtil.splitLines(output.output.trim()); + String[] actual_lines = StringUtil.splitLines(output_trim); String[] expected_lines = StringUtil.splitEqualsSign(test_case.getExpected()); Diff<String> diff = new Diff<String>(actual_lines, expected_lines); @@ -591,10 +566,12 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu expectf = null; } - twriter.addResult(new PhptTestResult(host, EPhptTestStatus.FAIL, test_case, output.output, actual_lines, expected_lines, output.charset, env, LocalHost.splitCmdString(cmd), stdin_post, shell_script, diff, expectf, preoverride_actual)); + twriter.addResult(new PhptTestResult(host, EPhptTestStatus.FAIL, test_case, output, actual_lines, expected_lines, charset, env, splitCmdString(), stdin_post, shell_script, diff, expectf, preoverride_actual, getCrashedSAPIOutput())); } } // end void evalTest + protected abstract String[] splitCmdString(); + protected static String remove_header_from_output(String output) { if (!output.contains("X-Power")) return output; diff --git a/src/com/mostc/pftt/runner/CliPhptTestCaseRunner.java b/src/com/mostc/pftt/runner/CliPhptTestCaseRunner.java index 9d47023..e906b7a 100644 --- a/src/com/mostc/pftt/runner/CliPhptTestCaseRunner.java +++ b/src/com/mostc/pftt/runner/CliPhptTestCaseRunner.java @@ -1,8 +1,19 @@ package com.mostc.pftt.runner; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.HashMap; + +import com.mostc.pftt.host.ExecOutput; import com.mostc.pftt.host.Host; +import com.mostc.pftt.host.LocalHost; import com.mostc.pftt.model.phpt.EPhptSection; import com.mostc.pftt.model.phpt.PhpBuild; +import com.mostc.pftt.model.phpt.PhpIni; import com.mostc.pftt.model.phpt.PhptTestCase; import com.mostc.pftt.model.phpt.PhptTestPack; import com.mostc.pftt.runner.PhptTestPackRunner.PhptThread; @@ -19,12 +30,15 @@ import com.mostc.pftt.util.StringUtil; */ public class CliPhptTestCaseRunner extends AbstractPhptTestCaseRunner2 { + protected ExecOutput output; + protected HashMap<String,String> env; - public CliPhptTestCaseRunner(PhptThread thread, PhptTestCase test_case, PhptTelemetryWriter twriter, Host host, ScenarioSet scenario_set, PhpBuild build, PhptTestPack test_pack) { - super(thread, test_case, twriter, host, scenario_set, build, test_pack); + public CliPhptTestCaseRunner(PhpIni ini, PhptThread thread, PhptTestCase test_case, PhptTelemetryWriter twriter, Host host, ScenarioSet scenario_set, PhpBuild build, PhptTestPack test_pack) { + super(ini, thread, test_case, twriter, host, scenario_set, build, test_pack); } - public void executeSkipIf() throws Exception { + @Override + protected String executeSkipIf() throws Exception { // Check if test should be skipped. if (test_skipif != null && test_case.containsSection(EPhptSection.SKIPIF)) { host.saveText(test_skipif, test_case.get(EPhptSection.SKIPIF)); @@ -40,17 +54,23 @@ public class CliPhptTestCaseRunner extends AbstractPhptTestCaseRunner2 { // execute SKIPIF (60 second timeout) output = host.exec(skip_cmd, Host.ONE_MINUTE, env, null, test_pack.getTestPack()); + + return output.output; } - } // end void executeSkipIf + return null; + } // end String executeSkipIf - public void executeTest() throws Exception { + @Override + protected String executeTest() throws Exception { // execute PHP to execute the TEST code ... allow up to 60 seconds for execution // if test is taking longer than 40 seconds to run, spin up an additional thread to compensate (so other non-slow tests can be executed) output = host.exec(shell_file, Host.ONE_MINUTE, null, stdin_post, test_case.isNon8BitCharset()?test_case.getCommonCharset():null, test_pack.getTestPack(), thread, 40); - } // end void executeTest + return output.output; + } // end String executeTest - public void executeClean() throws Exception { + @Override + protected void executeClean() throws Exception { if (test_case.containsSection(EPhptSection.CLEAN)) { host.saveText(test_clean, test_case.getTrim(EPhptSection.CLEAN), null); @@ -67,5 +87,86 @@ public class CliPhptTestCaseRunner extends AbstractPhptTestCaseRunner2 { host.delete(test_clean); } } // end void executeClean + + @Override + protected String getCrashedSAPIOutput() { + if (output.isCrashed()) + return output.isEmpty() ? + "PFTT: test printed nothing. was expected to print something. exited with non-zero code (probably crash): "+output.exit_code + : output.output; + else + return null; // no crash at all + } + + @Override + protected void createShellScript() throws IOException { + // useful: rm -rf `find ./ -name "*.sh"` + // + // create a .cmd (Windows batch script) or .sh (shell script) that will actually execute PHP + // this enables PHP to be executed like what PFTT does, but using a shell|batch script + shell_file = test_file + (host.isWindows() ? ".cmd" : ".sh" ); + if (shell_file.startsWith(test_pack.getTestPack())) { + shell_file = shell_file.substring(test_pack.getTestPack().length()); + if (shell_file.startsWith("/")||shell_file.startsWith("\\")) + shell_file = shell_file.substring(1); + shell_file = twriter.telem_dir+"/"+shell_file; + } + StringWriter sw = new StringWriter(); + PrintWriter fw = new PrintWriter(sw); + if (host.isWindows()) { + fw.println("@echo off"); // critical: or output will include these commands and fail test + } else { + fw.println("#!/bin/sh"); + } + fw.println("cd "+host.fixPath(test_dir)); + for ( String name : env.keySet()) { + String value = env.get(name); + if (value==null) + value = ""; + else + value = StringUtil.replaceAll(PAT_bs, "\\\\\"", StringUtil.replaceAll(PAT_dollar, "\\\\\\$", value)); + + if (host.isWindows()) { + if (value.contains(" ")) + // important: on Windows, don't use " " for script variables unless you have to + // the " " get passed into PHP so variables won't be read correctly + fw.println("set "+name+"=\""+value+"\""); + else + fw.println("set "+name+"="+value+""); + } else + fw.println("export "+name+"=\""+value+"\""); + } + fw.println(cmd); + fw.close(); + shell_script = sw.toString(); + FileWriter w = new FileWriter(shell_file); + fw = new PrintWriter(w); + fw.print(shell_script); + fw.flush(); + fw.close(); + w.close(); + } // end protected void createShellScript + + @Override + protected void prepareSTDIN() throws IOException { + String stdin_file = test_file + ".stdin"; + if (stdin_file.startsWith(test_pack.getTestPack())) { + stdin_file = stdin_file.substring(test_pack.getTestPack().length()); + if (stdin_file.startsWith("/")||stdin_file.startsWith("\\")) + stdin_file = stdin_file.substring(1); + stdin_file = twriter.telem_dir+"/"+stdin_file; + } + new File(stdin_file).getParentFile().mkdirs(); + if (stdin_post!=null) { + FileOutputStream fw = new FileOutputStream(stdin_file); + fw.write(stdin_post); + fw.close(); + } + } // end protected void prepareSTDIN + + @Override + protected String[] splitCmdString() { + return LocalHost.splitCmdString(cmd); + } } // end public class CliTestCaseRunner diff --git a/src/com/mostc/pftt/runner/HTTPTestCaseRunner.java b/src/com/mostc/pftt/runner/HTTPTestCaseRunner.java deleted file mode 100644 index 0ac831e..0000000 --- a/src/com/mostc/pftt/runner/HTTPTestCaseRunner.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.mostc.pftt.runner; - -import com.mostc.pftt.host.Host; -import com.mostc.pftt.model.phpt.PhpBuild; -import com.mostc.pftt.model.phpt.PhptTestCase; -import com.mostc.pftt.model.phpt.PhptTestPack; -import com.mostc.pftt.runner.PhptTestPackRunner.PhptThread; -import com.mostc.pftt.scenario.ScenarioSet; -import com.mostc.pftt.telemetry.PhptTelemetryWriter; - -/** Runs PHPT Test Cases against PHP while its running under a Web Server (builtin, IIS or Apache) - * - * @author Matt Ficken - * - */ - -public class HTTPTestCaseRunner extends AbstractPhptTestCaseRunner2 { - - public HTTPTestCaseRunner(PhptThread thread, PhptTestCase test_case, PhptTelemetryWriter twriter, Host host, ScenarioSet scenario_set, PhpBuild build, PhptTestPack test_pack) { - super(thread, test_case, twriter, host, scenario_set, build, test_pack); - // TODO Auto-generated constructor stub - } - - @Override - public void executeSkipIf() throws Exception { - // TODO Auto-generated method stub - - } - - @Override - public void executeTest() throws Exception { - // TODO Auto-generated method stub - - } - - @Override - public void executeClean() throws Exception { - // TODO Auto-generated method stub - - } - -} diff --git a/src/com/mostc/pftt/runner/PhptTestPackRunner.java b/src/com/mostc/pftt/runner/PhptTestPackRunner.java index 20fe5df..a0cdb61 100644 --- a/src/com/mostc/pftt/runner/PhptTestPackRunner.java +++ b/src/com/mostc/pftt/runner/PhptTestPackRunner.java @@ -2,7 +2,7 @@ package com.mostc.pftt.runner; import java.util.HashMap; import java.util.Iterator; -import java.util.LinkedList; +import java.util.List; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -11,6 +11,8 @@ import com.mostc.pftt.host.Host; import com.mostc.pftt.model.phpt.PhpBuild; import com.mostc.pftt.model.phpt.PhptTestCase; import com.mostc.pftt.model.phpt.PhptTestPack; +import com.mostc.pftt.model.sapi.SAPIInstance; +import com.mostc.pftt.model.sapi.TestCaseGroupKey; import com.mostc.pftt.scenario.AbstractSAPIScenario; import com.mostc.pftt.scenario.ScenarioSet; import com.mostc.pftt.telemetry.PhptTelemetryWriter; @@ -20,6 +22,7 @@ import com.mostc.pftt.util.HostEnvUtil; * * Can either run all PHPTs from the test pack, or PHPTs matching a set of names or name fragments. * + * TODO add @see for SAPIInstanceOrIni and concurrent or non-concurrent|serial SAPIInstanceOrInis * @author Matt Ficken * */ @@ -31,9 +34,10 @@ public class PhptTestPackRunner extends AbstractTestPackRunner { protected final PhptTelemetryWriter twriter; protected ETestPackRunnerState runner_state; protected AtomicInteger test_count, active_thread_count; - protected LinkedBlockingQueue<PhptTestCase> thread_safe_jobs; - protected HashMap<String,LinkedBlockingQueue<PhptTestCase>> jobs_by_extension; + protected HashMap<TestCaseGroupKey,LinkedBlockingQueue<PhptTestCase>> thread_safe_tests; + protected HashMap<String,HashMap<TestCaseGroupKey,LinkedBlockingQueue<PhptTestCase>>> non_thread_safe_tests; protected AbstractSAPIScenario sapi_scenario; + protected LinkedBlockingQueue<TestCaseGroupKey> group_keys; public PhptTestPackRunner(PhptTelemetryWriter twriter, PhptTestPack test_pack, ScenarioSet scenario_set, PhpBuild build, Host host) { super(scenario_set, build, host); @@ -41,71 +45,140 @@ public class PhptTestPackRunner extends AbstractTestPackRunner { this.test_pack = test_pack; } - public void run_test_list(LinkedList<PhptTestCase> test_cases) throws Exception { + public void runTestList(List<PhptTestCase> test_cases) throws Exception { runner_state = ETestPackRunnerState.RUNNING; sapi_scenario = ScenarioSet.getSAPIScenario(scenario_set); HostEnvUtil.prepareHostEnv(host); -// Scenario[] scenarios = new Scenario[]{new Scenario()}; -// -// for (Scenario scenario : scenarios ) { - //rt.scenario = scenario; + System.out.println("PFTT: loaded tests: "+test_cases.size()); - System.out.println("PFTT: loaded tests: "+test_cases.size()); - - thread_safe_jobs = new LinkedBlockingQueue<PhptTestCase>(); - jobs_by_extension = new HashMap<String,LinkedBlockingQueue<PhptTestCase>>(); + try { + groupTestCases(test_cases); + + // TODO serialSAPIInstance_executeTestCases(); + parallelSAPIInstance_executeTestCases(); + + } finally { + // be sure all running WebServerInstances, or other SAPIInstances are + // closed by end of testing (otherwise php.exe -S will keep on running) + close(); + } + } + + public void close() { + for ( TestCaseGroupKey group_key : group_keys ) { + if (group_key instanceof SAPIInstance) + ((SAPIInstance)group_key).close(); + } + } + + protected void groupTestCases(List<PhptTestCase> test_cases) throws InterruptedException { + thread_safe_tests = new HashMap<TestCaseGroupKey,LinkedBlockingQueue<PhptTestCase>>(); + non_thread_safe_tests = new HashMap<String,HashMap<TestCaseGroupKey,LinkedBlockingQueue<PhptTestCase>>>(); + + // enqueue tests + // + group_keys = new LinkedBlockingQueue<TestCaseGroupKey>(); + + boolean is_thread_safe = false; + TestCaseGroupKey group_key; + HashMap<TestCaseGroupKey,LinkedBlockingQueue<PhptTestCase>> j; + LinkedBlockingQueue<PhptTestCase> k; + for (PhptTestCase test_case : test_cases) { + is_thread_safe = true; - // enqueue tests // - // group non-thread safe tests together by PHP extension - // (extensions are tested in parallel) - for (String ext:PhptTestCase.NON_THREAD_SAFE_EXTENSIONS) { - LinkedBlockingQueue<PhptTestCase> j = new LinkedBlockingQueue<PhptTestCase>(); - jobs_by_extension.put(ext, j); - - Iterator<PhptTestCase> it = test_cases.listIterator(); - PhptTestCase f; - while (it.hasNext()) { - f = it.next(); - if (f.getName().toLowerCase().contains(ext)) { - j.put(f); - it.remove(); - } - } - } - { - // put all remaining files - Iterator<PhptTestCase> it = test_cases.listIterator(); - while (it.hasNext()) { - thread_safe_jobs.put(it.next()); + try { + if (sapi_scenario.willSkip(twriter, host, build, test_case)) { + // #willSkip will record the PhptTestResult explaining why it was skipped + continue; } + } catch ( Exception ex ) { + ex.printStackTrace(); } // - // execute tests - long start_time = System.currentTimeMillis(); - - // 80 -> 1000 + // + // TODO for cli, don't create anything just a static key + // TODO what about web servers that only allow 1 running instance ??? + group_key = sapi_scenario.createTestGroupKey(host, build, test_pack, test_case); + if (!group_keys.contains(group_key)) + group_keys.put(group_key); + // - int thread_count = INIT_THREAD_COUNT; - test_count = new AtomicInteger(0); - active_thread_count = new AtomicInteger(thread_count); + // + /* TODO for (String ext:PhptTestCase.NON_THREAD_SAFE_EXTENSIONS) { + if (test_case.getName().toLowerCase().contains(ext)) { + j = non_thread_safe_tests.get(ext); + if (j==null) + non_thread_safe_tests.put(ext, j = new HashMap<TestCaseGroupKey,LinkedBlockingQueue<PhptTestCase>>()); + k = j.get(group_key); + if (k==null) + j.put(group_key, k = new LinkedBlockingQueue<PhptTestCase>()); + k.put(test_case); + + is_thread_safe = false; + + break; + } + }*/ + // - for ( int i=0 ; i < thread_count ; i++ ) - start_thread(); + // + if (is_thread_safe) { + k = thread_safe_tests.get(group_key); + if (k==null) + thread_safe_tests.put(group_key, k = new LinkedBlockingQueue<PhptTestCase>()); + k.put(test_case); + } + // + } // end while + } // end protected void groupTestCases + + protected void parallelSAPIInstance_executeTestCases() throws InterruptedException { + int thread_count = INIT_THREAD_COUNT; + test_count = new AtomicInteger(0); + active_thread_count = new AtomicInteger(thread_count); + + thread_count = INIT_THREAD_COUNT; + for ( int i=0 ; i < thread_count ; i++ ) { + start_thread(); + } + + // wait until done + int c ; while ( ( c = active_thread_count.get() ) > 0 ) { Thread.sleep(c>3?1000:50); } + } // end protected void parallelSAPIInstance_executeTestCases + + /*protected void serialSAPIInstance_executeTestCases() throws InterruptedException { + // execute tests + long start_time = System.currentTimeMillis(); + + int thread_count = INIT_THREAD_COUNT; + test_count = new AtomicInteger(0); + active_thread_count = new AtomicInteger(thread_count); + + // + Iterator<TestCaseGroupKey> ini_it = group_keys.iterator(); + TestCaseGroupKey ini; + while (ini_it.hasNext()) { + ini = ini_it.next(); + thread_count = INIT_THREAD_COUNT; + for ( int i=0 ; i < thread_count ; i++ ) { + start_thread(ini); + } // wait until done int c ; while ( ( c = active_thread_count.get() ) > 0 ) { Thread.sleep(c>3?1000:50); } - - long run_time = Math.abs(System.currentTimeMillis() - start_time); - - //System.out.println(test_count); - System.out.println((run_time/1000)+" seconds"); - - //} // end for - } // end void + } + // + + long run_time = Math.abs(System.currentTimeMillis() - start_time); + + //System.out.println(test_count); + System.out.println((run_time/1000)+" seconds"); + } // end void serialSAPIInstance_executeTestCases + */ protected void start_thread() { PhptThread t = new PhptThread(); @@ -115,48 +188,96 @@ public class PhptTestPackRunner extends AbstractTestPackRunner { } public class PhptThread extends SlowReplacementTestPackRunnerThread { - protected AtomicBoolean run_thread = new AtomicBoolean(true); - + protected final AtomicBoolean run_thread = new AtomicBoolean(true); + @Override public void run() { - // pick a non-thread-safe(NTS) extension that isn't already running - // then run it + // pick a non-thread-safe(NTS) extension that isn't already running then run it // // keep doing that until they're all done, then execute all the thread-safe extensions - // (if there aren't enough NTS extensions to fill all the threads, some threads will execute thread-safe tests + // (if there aren't enough NTS extensions to fill all the threads, some threads will only execute thread-safe tests // - + try { + runNonThreadSafe(); + + // execute any remaining thread safe jobs + runThreadSafe(); + + } finally { + if (run_thread.get()) + // if #stopThisThread not called + active_thread_count.decrementAndGet(); + } + } // end public void run + + protected void runThreadSafe() { + TestCaseGroupKey group_key; + Iterator<TestCaseGroupKey> group_it; + LinkedBlockingQueue<PhptTestCase> jobs; while (run_thread.get()) { - LinkedBlockingQueue<PhptTestCase> jobs; - synchronized (jobs_by_extension) { - if (jobs_by_extension.isEmpty()) + synchronized (thread_safe_tests) { + group_it = thread_safe_tests.keySet().iterator(); + if (!group_it.hasNext()) + break; + + group_key = group_it.next(); + jobs = thread_safe_tests.get(group_key); + if (jobs.isEmpty()) + group_it.remove(); + } + if (group_key!=null) + exec_jobs(group_key, jobs, test_count); + } + } // end protected void runThreadSafe + + protected void runNonThreadSafe() { + TestCaseGroupKey group_key; + Iterator<String> ext_it; + Iterator<TestCaseGroupKey> group_it; + HashMap<TestCaseGroupKey,LinkedBlockingQueue<PhptTestCase>> non_thread_safe_tests_group; + LinkedBlockingQueue<PhptTestCase> jobs; + String ext_name; + while (run_thread.get()) { + synchronized (non_thread_safe_tests) { + if (non_thread_safe_tests.isEmpty()) // no more NTS extensions break; // pick a remaining NTS extension - Iterator<String> it = jobs_by_extension.keySet().iterator(); - jobs = jobs_by_extension.get(it.next()); - it.remove(); - } - exec_jobs(jobs, test_count); + ext_it = non_thread_safe_tests.keySet().iterator(); + ext_name = ext_it.next(); + + non_thread_safe_tests_group = non_thread_safe_tests.get(ext_name); + group_it = non_thread_safe_tests_group.keySet().iterator(); + group_key = null; + jobs = null; + while (group_it.hasNext()) { + group_key = group_it.next(); + jobs = non_thread_safe_tests_group.get(group_key); + if (!jobs.isEmpty()) { + group_it.remove(); + break; + } + } + } // end sync + if (group_key!=null) + exec_jobs(group_key, jobs, test_count); } - - // execute any remaining thread safe jobs - exec_jobs(thread_safe_jobs, test_count); - - if (run_thread.get()) - // if #stopThisThread not called - active_thread_count.decrementAndGet(); - } // end public void run + } // end protected void runNonThreadSafe - protected void exec_jobs(LinkedBlockingQueue<PhptTestCase> jobs, AtomicInteger test_count) { - PhptTestCase test_file; - while ( ( test_file = jobs.poll() ) != null && run_thread.get() && runner_state==ETestPackRunnerState.RUNNING) { + protected void exec_jobs(TestCaseGroupKey ini, LinkedBlockingQueue<PhptTestCase> jobs, AtomicInteger test_count) { + PhptTestCase test_case; + while ( ( + test_case = jobs.poll() + ) != null && + run_thread.get() && + runner_state==ETestPackRunnerState.RUNNING + ) { // CRITICAL: catch exception so thread will always end normally try { - sapi_scenario.createPhptTestCaseRunner(this, test_file, twriter, host, scenario_set, build, test_pack).runTest(); + sapi_scenario.createPhptTestCaseRunner(this, ini, test_case, twriter, host, scenario_set, build, test_pack).runTest(); } catch ( Throwable ex ) { - //twriter.show_exception(test_file, ex); + twriter.show_exception(test_case, ex); } test_count.incrementAndGet(); diff --git a/src/com/mostc/pftt/runner/PhptTestPackRunner.java.bak b/src/com/mostc/pftt/runner/PhptTestPackRunner.java.bak new file mode 100644 index 0000000..cf6b195 --- /dev/null +++ b/src/com/mostc/pftt/runner/PhptTestPackRunner.java.bak @@ -0,0 +1,210 @@ +package com.mostc.pftt.runner; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import com.mostc.pftt.host.Host; +import com.mostc.pftt.model.phpt.PhpBuild; +import com.mostc.pftt.model.phpt.PhptTestCase; +import com.mostc.pftt.model.phpt.PhptTestPack; +import com.mostc.pftt.scenario.AbstractSAPIScenario; +import com.mostc.pftt.scenario.ScenarioSet; +import com.mostc.pftt.telemetry.PhptTelemetryWriter; +import com.mostc.pftt.util.HostEnvUtil; + +/** Runs PHPTs from a given PhptTestPack. + * + * Can either run all PHPTs from the test pack, or PHPTs matching a set of names or name fragments. + * + * @author Matt Ficken + * + */ + +// TODO +//1. group test-cases by ini +//2. start new web server for each ini +// -use 1 WebserverSAPIMonitor instance to allocate ports +// -WebServerSAPIInstance instance for each web server process +//3a. if allowConcurrentWebserverSAPIInstances +//.1. group by SAPIInstance, creating a SAPIInstance for each INI +//.2. thread by all test-cases, tracking how many test-cases are left for a SAPI Instance and closing it when it reaches 0 +//3b. if !allowConcurrentWebserverSAPIInstances +//.1. thread by test-case until all test cases for INI/SAPIInstance executed, then close current and proceed to next SAPIInstance +// -create SAPIInstance for INI if DNE + +public class PhptTestPackRunner extends AbstractTestPackRunner { + protected static final int INIT_THREAD_COUNT = 8; // TODO 16; + protected static final int MAX_THREAD_COUNT = 32; + protected final PhptTestPack test_pack; + protected final PhptTelemetryWriter twriter; + protected ETestPackRunnerState runner_state; + protected AtomicInteger test_count, active_thread_count; + protected LinkedBlockingQueue<PhptTestCase> thread_safe_jobs; + protected HashMap<String,LinkedBlockingQueue<PhptTestCase>> jobs_by_extension; + protected AbstractSAPIScenario sapi_scenario; + + public PhptTestPackRunner(PhptTelemetryWriter twriter, PhptTestPack test_pack, ScenarioSet scenario_set, PhpBuild build, Host host) { + super(scenario_set, build, host); + this.twriter = twriter; + this.test_pack = test_pack; + } + + public void run_test_list(LinkedList<PhptTestCase> test_cases) throws Exception { + runner_state = ETestPackRunnerState.RUNNING; + sapi_scenario = ScenarioSet.getSAPIScenario(scenario_set); + + HostEnvUtil.prepareHostEnv(host); + +// Scenario[] scenarios = new Scenario[]{new Scenario()}; +// +// for (Scenario scenario : scenarios ) { + //rt.scenario = scenario; + + System.out.println("PFTT: loaded tests: "+test_cases.size()); + + thread_safe_jobs = new LinkedBlockingQueue<PhptTestCase>(); + jobs_by_extension = new HashMap<String,LinkedBlockingQueue<PhptTestCase>>(); + + // enqueue tests + // + // group non-thread safe tests together by PHP extension + // (extensions are tested in parallel) + for (String ext:PhptTestCase.NON_THREAD_SAFE_EXTENSIONS) { + LinkedBlockingQueue<PhptTestCase> j = new LinkedBlockingQueue<PhptTestCase>(); + jobs_by_extension.put(ext, j); + + Iterator<PhptTestCase> it = test_cases.listIterator(); + PhptTestCase f; + while (it.hasNext()) { + f = it.next(); + if (f.getName().toLowerCase().contains(ext)) { + j.put(f); + it.remove(); + } + } + } + { + // put all remaining files + Iterator<PhptTestCase> it = test_cases.listIterator(); + while (it.hasNext()) { + thread_safe_jobs.put(it.next()); + } + } + // + + // execute tests + long start_time = System.currentTimeMillis(); + + // 80 -> 1000 + + int thread_count = INIT_THREAD_COUNT; + test_count = new AtomicInteger(0); + active_thread_count = new AtomicInteger(thread_count); + + for ( int i=0 ; i < thread_count ; i++ ) + start_thread(); + + // wait until done + int c ; while ( ( c = active_thread_count.get() ) > 0 ) { Thread.sleep(c>3?1000:50); } + + long run_time = Math.abs(System.currentTimeMillis() - start_time); + + //System.out.println(test_count); + System.out.println((run_time/1000)+" seconds"); + + //} // end for + } // end void + + protected void start_thread() { + PhptThread t = new PhptThread(); + // if running Swing UI, run thread minimum priority in favor of Swing EDT + t.setPriority(Thread.MIN_PRIORITY); + t.start(); + } + + public class PhptThread extends SlowReplacementTestPackRunnerThread { + protected AtomicBoolean run_thread = new AtomicBoolean(true); + + @Override + public void run() { + // pick a non-thread-safe(NTS) extension that isn't already running + // then run it + // + // keep doing that until they're all done, then execute all the thread-safe extensions + // (if there aren't enough NTS extensions to fill all the threads, some threads will execute thread-safe tests + // + + while (run_thread.get()) { + LinkedBlockingQueue<PhptTestCase> jobs; + synchronized (jobs_by_extension) { + if (jobs_by_extension.isEmpty()) + // no more NTS extensions + break; + + // pick a remaining NTS extension + Iterator<String> it = jobs_by_extension.keySet().iterator(); + jobs = jobs_by_extension.get(it.next()); + it.remove(); + } + exec_jobs(jobs, test_count); + } + + // execute any remaining thread safe jobs + exec_jobs(thread_safe_jobs, test_count); + + if (run_thread.get()) + // if #stopThisThread not called + active_thread_count.decrementAndGet(); + } // end public void run + + protected void exec_jobs(LinkedBlockingQueue<PhptTestCase> jobs, AtomicInteger test_count) { + PhptTestCase test_file; + while ( ( test_file = jobs.poll() ) != null && run_thread.get() && runner_state==ETestPackRunnerState.RUNNING) { + // CRITICAL: catch exception so thread will always end normally + try { + sapi_scenario.createPhptTestCaseRunner(this, test_file, twriter, host, scenario_set, build, test_pack).runTest(); + } catch ( Throwable ex ) { + //twriter.show_exception(test_file, ex); + } + + test_count.incrementAndGet(); + + //Thread.yield() + } + } // end protected void exec_jobs + + @Override + protected boolean slowCreateNewThread() { + return false; // TODO active_thread_count.get() < MAX_THREAD_COUNT; + } + + @Override + protected void createNewThread() { + start_thread(); + } + + @Override + protected void stopThisThread() { + // continue running current CliTestCaseRunner, but don't start any more of them + run_thread.set(false); + + active_thread_count.decrementAndGet(); + } + + } // end public class PhptThread + + @Override + public void setState(ETestPackRunnerState state) throws IllegalStateException { + this.runner_state = state; + } + + @Override + public ETestPackRunnerState getState() { + return runner_state; + } + +} // end class TestPackRunner diff --git a/src/com/mostc/pftt/scenario/AbstractApacheScenario.java b/src/com/mostc/pftt/scenario/AbstractApacheScenario.java index 0d6725f..612d6e1 100644 --- a/src/com/mostc/pftt/scenario/AbstractApacheScenario.java +++ b/src/com/mostc/pftt/scenario/AbstractApacheScenario.java @@ -1,5 +1,7 @@ package com.mostc.pftt.scenario; +import com.mostc.pftt.model.sapi.ApacheManager; + /** Scenarios for testing managing and testing Apache * * @author Matt Ficken @@ -8,4 +10,8 @@ package com.mostc.pftt.scenario; public abstract class AbstractApacheScenario extends AbstractWebServerScenario { + public AbstractApacheScenario() { + super(new ApacheManager()); + } + } diff --git a/src/com/mostc/pftt/scenario/AbstractIISScenario.java b/src/com/mostc/pftt/scenario/AbstractIISScenario.java index ed1dd36..d653c9c 100644 --- a/src/com/mostc/pftt/scenario/AbstractIISScenario.java +++ b/src/com/mostc/pftt/scenario/AbstractIISScenario.java @@ -1,5 +1,7 @@ package com.mostc.pftt.scenario; +import com.mostc.pftt.model.sapi.IISManager; + /** Abstract scenario for managing and testing IIS * * @author Matt Ficken @@ -7,5 +9,15 @@ package com.mostc.pftt.scenario; */ public abstract class AbstractIISScenario extends AbstractWebServerScenario { + + public AbstractIISScenario() { + super(new IISManager()); + } + public abstract boolean isExpress(); + + public boolean isStandard() { + return !isExpress(); + } + } diff --git a/src/com/mostc/pftt/scenario/AbstractSAPIScenario.java b/src/com/mostc/pftt/scenario/AbstractSAPIScenario.java index 273322c..8475fcc 100644 --- a/src/com/mostc/pftt/scenario/AbstractSAPIScenario.java +++ b/src/com/mostc/pftt/scenario/AbstractSAPIScenario.java @@ -4,8 +4,8 @@ import com.mostc.pftt.host.Host; import com.mostc.pftt.model.phpt.PhpBuild; import com.mostc.pftt.model.phpt.PhptTestCase; import com.mostc.pftt.model.phpt.PhptTestPack; +import com.mostc.pftt.model.sapi.TestCaseGroupKey; import com.mostc.pftt.runner.AbstractPhptTestCaseRunner; -import com.mostc.pftt.runner.CliPhptTestCaseRunner; import com.mostc.pftt.runner.PhptTestPackRunner.PhptThread; import com.mostc.pftt.telemetry.PhptTelemetryWriter; @@ -26,6 +26,7 @@ public abstract class AbstractSAPIScenario extends AbstractSerialScenario { /** creates a runner to run a single PhptTestCase under this SAPI scenario * * @param thread + * @param ini * @param test_case * @param twriter * @param host @@ -34,8 +35,13 @@ public abstract class AbstractSAPIScenario extends AbstractSerialScenario { * @param test_pack * @return */ - public AbstractPhptTestCaseRunner createPhptTestCaseRunner(PhptThread thread, PhptTestCase test_case, PhptTelemetryWriter twriter, Host host, ScenarioSet scenario_set, PhpBuild build, PhptTestPack test_pack) { - return new CliPhptTestCaseRunner(thread, test_case, twriter, host, scenario_set, build, test_pack); + // TODO rename ini + public abstract AbstractPhptTestCaseRunner createPhptTestCaseRunner(PhptThread thread, TestCaseGroupKey ini, PhptTestCase test_case, PhptTelemetryWriter twriter, Host host, ScenarioSet scenario_set, PhpBuild build, PhptTestPack test_pack); + + public abstract TestCaseGroupKey createTestGroupKey(Host host, PhpBuild build, PhptTestPack test_pack, PhptTestCase test_case); + + public boolean willSkip(PhptTelemetryWriter twriter, Host host, PhpBuild build, PhptTestCase test_case) throws Exception { + return AbstractPhptTestCaseRunner.willSkip(twriter, host, build, test_case); } } diff --git a/src/com/mostc/pftt/scenario/AbstractWebServerScenario.java b/src/com/mostc/pftt/scenario/AbstractWebServerScenario.java index 108c2a7..4f3fa99 100644 --- a/src/com/mostc/pftt/scenario/AbstractWebServerScenario.java +++ b/src/com/mostc/pftt/scenario/AbstractWebServerScenario.java @@ -4,22 +4,44 @@ import com.mostc.pftt.host.Host; import com.mostc.pftt.model.phpt.PhpBuild; import com.mostc.pftt.model.phpt.PhptTestCase; import com.mostc.pftt.model.phpt.PhptTestPack; +import com.mostc.pftt.model.sapi.TestCaseGroupKey; +import com.mostc.pftt.model.sapi.WebServerInstance; +import com.mostc.pftt.model.sapi.WebServerManager; import com.mostc.pftt.runner.AbstractPhptTestCaseRunner; -import com.mostc.pftt.runner.HTTPTestCaseRunner; +import com.mostc.pftt.runner.HttpTestCaseRunner; import com.mostc.pftt.runner.PhptTestPackRunner.PhptThread; import com.mostc.pftt.telemetry.PhptTelemetryWriter; -/** +/** scenarios for testing PHP while its running under a web server + * * @see SSLSocketScenario * @author Matt Ficken * */ public abstract class AbstractWebServerScenario extends AbstractSAPIScenario { - + protected final WebServerManager smgr; + + protected AbstractWebServerScenario(WebServerManager smgr) { + this.smgr = smgr; + } + @Override - public AbstractPhptTestCaseRunner createPhptTestCaseRunner(PhptThread thread, PhptTestCase test_case, PhptTelemetryWriter twriter, Host host, ScenarioSet scenario_set, PhpBuild build, PhptTestPack test_pack) { - return new HTTPTestCaseRunner(thread, test_case, twriter, host, scenario_set, build, test_pack); + public AbstractPhptTestCaseRunner createPhptTestCaseRunner(PhptThread thread, TestCaseGroupKey ini, PhptTestCase test_case, PhptTelemetryWriter twriter, Host host, ScenarioSet scenario_set, PhpBuild build, PhptTestPack test_pack) { + return new HttpTestCaseRunner(smgr, (WebServerInstance)ini, thread, test_case, twriter, host, scenario_set, build, test_pack); + } + + WebServerInstance web; // TODO temp + @Override + public synchronized TestCaseGroupKey createTestGroupKey(Host host, PhpBuild build, PhptTestPack test_pack, PhptTestCase test_case) { + // TODO use HttpTestCaseRunner to get docroot from test_pack + if (web!=null) + return web; + return web = smgr.getWebServerInstance(host, build, AbstractPhptTestCaseRunner.createIniForTest(host, build, test_pack, test_case), test_pack.getTestPack(), null); + } + + public boolean willSkip(PhptTelemetryWriter twriter, Host host, PhpBuild build, PhptTestCase test_case) throws Exception { + return HttpTestCaseRunner.willSkip(twriter, host, build, test_case); } -} +} // end public abstract class AbstractWebServerScenario diff --git a/src/com/mostc/pftt/scenario/BuiltinWWWScenario.java b/src/com/mostc/pftt/scenario/BuiltinWWWScenario.java deleted file mode 100644 index f07a719..0000000 --- a/src/com/mostc/pftt/scenario/BuiltinWWWScenario.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.mostc.pftt.scenario; - -/** Tests PHP using PHP 5.4's builtin web server. (NOT IMPLEMENTED) - * - * This is the web server that's run when a user runs: php -S - * - * @author Matt Ficken - * - */ - -public class BuiltinWWWScenario extends AbstractWebServerScenario { - - @Override - public String getName() { - return "Builtin-WWW"; - } - - @Override - public boolean isImplemented() { - return false; - } - -} diff --git a/src/com/mostc/pftt/scenario/BuiltinWebServerScenario.java b/src/com/mostc/pftt/scenario/BuiltinWebServerScenario.java new file mode 100644 index 0000000..55b9466 --- /dev/null +++ b/src/com/mostc/pftt/scenario/BuiltinWebServerScenario.java @@ -0,0 +1,47 @@ +package com.mostc.pftt.scenario; + +import com.mostc.pftt.host.Host; +import com.mostc.pftt.model.phpt.EBuildBranch; +import com.mostc.pftt.model.phpt.PhpBuild; +import com.mostc.pftt.model.sapi.BuiltinWebServerManager; + +/** Tests PHP using PHP's builtin web server. + * + * This is the web server that's run when a user runs: php -S + * + * This feature is only available (this scenario can only be run against) PHP 5.4+ (not PHP 5.3) + * + * @author Matt Ficken + * + */ + +public class BuiltinWebServerScenario extends AbstractWebServerScenario { + + protected BuiltinWebServerScenario() { + super(new BuiltinWebServerManager()); + } + + /** don't run this scenario on PHP 5.3 + * + */ + @Override + public boolean isSupported(Host host, PhpBuild build) { + try { + return build.getVersionBranch(host) != EBuildBranch.PHP_5_3; + } catch (Exception e) { + e.printStackTrace(); + return true; + } + } + + @Override + public String getName() { + return "Builtin-Web-Server"; + } + + @Override + public boolean isImplemented() { + return true; + } + +} diff --git a/src/com/mostc/pftt/scenario/CLIScenario.java b/src/com/mostc/pftt/scenario/CLIScenario.java index 7bd6594..7844719 100644 --- a/src/com/mostc/pftt/scenario/CLIScenario.java +++ b/src/com/mostc/pftt/scenario/CLIScenario.java @@ -1,5 +1,16 @@ package com.mostc.pftt.scenario; +import com.mostc.pftt.host.Host; +import com.mostc.pftt.model.phpt.PhpBuild; +import com.mostc.pftt.model.phpt.PhpIni; +import com.mostc.pftt.model.phpt.PhptTestCase; +import com.mostc.pftt.model.phpt.PhptTestPack; +import com.mostc.pftt.model.sapi.TestCaseGroupKey; +import com.mostc.pftt.runner.AbstractPhptTestCaseRunner; +import com.mostc.pftt.runner.CliPhptTestCaseRunner; +import com.mostc.pftt.runner.PhptTestPackRunner.PhptThread; +import com.mostc.pftt.telemetry.PhptTelemetryWriter; + /** Tests the Command Line Interface(CLI) for running PHP. * * @author Matt Ficken @@ -18,4 +29,17 @@ public class CLIScenario extends AbstractSAPIScenario { return true; } + @Override + public AbstractPhptTestCaseRunner createPhptTestCaseRunner( + PhptThread thread, TestCaseGroupKey ini, PhptTestCase test_case, + PhptTelemetryWriter twriter, Host host, ScenarioSet scenario_set, + PhpBuild build, PhptTestPack test_pack) { + return new CliPhptTestCaseRunner((PhpIni)ini, thread, test_case, twriter, host, scenario_set, build, test_pack); + } + + @Override + public TestCaseGroupKey createTestGroupKey(Host host, PhpBuild build, PhptTestPack test_pack, PhptTestCase test_case) { + return AbstractPhptTestCaseRunner.createIniForTest(host, build, test_pack, test_case); + } + } diff --git a/src/com/mostc/pftt/scenario/IISExpressFastCGIScenario.java b/src/com/mostc/pftt/scenario/IISExpressFastCGIScenario.java index c263847..facadab 100644 --- a/src/com/mostc/pftt/scenario/IISExpressFastCGIScenario.java +++ b/src/com/mostc/pftt/scenario/IISExpressFastCGIScenario.java @@ -18,4 +18,9 @@ public class IISExpressFastCGIScenario extends AbstractIISFastCGIScenario { return false; } + @Override + public boolean isExpress() { + return true; + } + } diff --git a/src/com/mostc/pftt/scenario/IISStandardFastCGIScenario.java b/src/com/mostc/pftt/scenario/IISStandardFastCGIScenario.java index ad69ca0..4c7f817 100644 --- a/src/com/mostc/pftt/scenario/IISStandardFastCGIScenario.java +++ b/src/com/mostc/pftt/scenario/IISStandardFastCGIScenario.java @@ -18,4 +18,9 @@ public class IISStandardFastCGIScenario extends AbstractIISFastCGIScenario { return false; } + @Override + public boolean isExpress() { + return false; + } + } diff --git a/src/com/mostc/pftt/scenario/Scenario.java b/src/com/mostc/pftt/scenario/Scenario.java index cfa18bc..f23bb59 100644 --- a/src/com/mostc/pftt/scenario/Scenario.java +++ b/src/com/mostc/pftt/scenario/Scenario.java @@ -1,5 +1,8 @@ package com.mostc.pftt.scenario; +import com.mostc.pftt.host.Host; +import com.mostc.pftt.model.phpt.PhpBuild; + /** Scenario to test PHP under. * * Often a whole set of Scenarios (@see ScenarioSet) are used together. @@ -24,6 +27,10 @@ public abstract class Scenario { public abstract String getName(); public abstract boolean isImplemented(); + public boolean isSupported(Host host, PhpBuild build) { + return true; + } + @Override public String toString() { return getName(); @@ -48,8 +55,8 @@ public abstract class Scenario { }, // SAPIs new Scenario[]{ - new CLIScenario(), - new BuiltinWWWScenario(), + // TODO new CLIScenario(), + new BuiltinWebServerScenario(), new ApacheModPHPScenario(), new IISExpressFastCGIScenario(), new IISStandardFastCGIScenario() @@ -83,4 +90,4 @@ public abstract class Scenario { }; } -} +} // end public abstract class Scenario diff --git a/src/com/mostc/pftt/scenario/ScenarioSet.java b/src/com/mostc/pftt/scenario/ScenarioSet.java index 0268dc3..2d73295 100644 --- a/src/com/mostc/pftt/scenario/ScenarioSet.java +++ b/src/com/mostc/pftt/scenario/ScenarioSet.java @@ -5,6 +5,7 @@ import java.util.Arrays; import java.util.List; import com.mostc.pftt.host.Host; +import com.mostc.pftt.model.phpt.PhpBuild; /** A Set of Scenarios to test PHP under. * @@ -58,11 +59,15 @@ public class ScenarioSet extends ArrayList<Scenario> { /** determines if this set of scenarios can be executed on the given host * * @param host + * @param build * @return */ - public boolean isSupported(Host host) { - // TODO - return false; + public boolean isSupported(Host host, PhpBuild build) { + for (Scenario s :this) { + if (!s.isSupported(host, build)) + return false; + } + return true; } @Override diff --git a/src/com/mostc/pftt/telemetry/PhptTelemetryReader.java b/src/com/mostc/pftt/telemetry/PhptTelemetryReader.java index 71e8e1e..b044a15 100644 --- a/src/com/mostc/pftt/telemetry/PhptTelemetryReader.java +++ b/src/com/mostc/pftt/telemetry/PhptTelemetryReader.java @@ -98,7 +98,7 @@ public class PhptTelemetryReader extends PhptTelemetry { return tally.unsupported; case BORK: return tally.bork; - case EXCEPTION: + case INTERNAL_EXCEPTION: return tally.exception; } return 0; diff --git a/src/com/mostc/pftt/telemetry/PhptTelemetryWriter.java b/src/com/mostc/pftt/telemetry/PhptTelemetryWriter.java index edcf96d..1eeb2c0 100644 --- a/src/com/mostc/pftt/telemetry/PhptTelemetryWriter.java +++ b/src/com/mostc/pftt/telemetry/PhptTelemetryWriter.java @@ -93,7 +93,7 @@ public class PhptTelemetryWriter extends PhptTelemetry { tally.xfail_works = counts.get(EPhptTestStatus.XFAIL_WORKS).get(); tally.unsupported = counts.get(EPhptTestStatus.UNSUPPORTED).get(); tally.bork = counts.get(EPhptTestStatus.BORK).get(); - tally.exception = counts.get(EPhptTestStatus.EXCEPTION).get(); + tally.exception = counts.get(EPhptTestStatus.INTERNAL_EXCEPTION).get(); FileWriter fw = new FileWriter(new File(telem_dir, "tally.xml")); PhptTallyFile.write(tally, fw); fw.close(); @@ -160,7 +160,8 @@ public class PhptTelemetryWriter extends PhptTelemetry { String ex_str = ErrorUtil.toString(ex); // count exceptions as a result (the worst kind of failure, a pftt failure) - addResult(new PhptTestResult(host, EPhptTestStatus.EXCEPTION, test_case, ex_str, null, null, null, null, null, null, null, null, null, null)); + addResult(new PhptTestResult(host, EPhptTestStatus.INTERNAL_EXCEPTION, test_case, ex_str, null, null, null, null, null, null, null, null, null, null)); + // TODO show count of exceptions in gui } int completed = 0; // XXX public void addResult(PhptTestResult result) { diff --git a/src/com/mostc/pftt/telemetry/PhptTestResult.java b/src/com/mostc/pftt/telemetry/PhptTestResult.java index 23b8311..edfcee0 100644 --- a/src/com/mostc/pftt/telemetry/PhptTestResult.java +++ b/src/com/mostc/pftt/telemetry/PhptTestResult.java @@ -17,6 +17,17 @@ import com.mostc.pftt.host.Host; import com.mostc.pftt.model.phpt.EPhptTestStatus; import com.mostc.pftt.model.phpt.PhptTestCase; +/** result of running a PhptTestCase + * + * the status of PhptTestCase is determined by: + * -executing SKIPIF section(optional) + * -comparing the actual and EXPECT, EXPECTF or EXPECTREGEX sections + * -certain sections(ENV, STDIN) if present, are skipped for HTTP testing + * + * @author Matt Ficken + * + */ + public class PhptTestResult { public final Host host; /** the result of the test case */ @@ -45,9 +56,17 @@ public class PhptTestResult { /** the difference (as string) between the actual and expected output */ @Nullable public final String diff_str; + protected final String sapi_output; public PhptTestResult(Host host, EPhptTestStatus status, PhptTestCase test_case, String actual, String[] actual_lines, String[] expected_lines, Charset actual_cs, Map<String,String> env, String[] cmd_array, byte[] stdin_data, String shell_script, Diff<String> diff, String expectf_output, String preoverride_actual) { + this(host, status, test_case, actual, actual_lines, expected_lines, actual_cs, env, cmd_array, stdin_data, shell_script, diff, expectf_output, preoverride_actual, null); + } + + public PhptTestResult(Host host, EPhptTestStatus status, PhptTestCase test_case, String actual, String[] actual_lines, String[] expected_lines, Charset actual_cs, Map<String,String> env, String[] cmd_array, byte[] stdin_data, String shell_script, Diff<String> diff, String expectf_output, String preoverride_actual, String sapi_output) { actual = (""+actual_cs)+"\n"+actual; // TODO + + this.sapi_output = sapi_output; + this.host = host; this.status = status; this.test_case = test_case; @@ -64,7 +83,12 @@ public class PhptTestResult { this.diff_str = toString(actual_lines, diff, expected_lines); else this.diff_str = null; - } + } + + public String getSAPIOutput() { + // TODO should store this once for all test cases run under the same instance of a SAPI + return sapi_output; + } public void write(File telem_dir) throws IOException { if (status==EPhptTestStatus.FAIL) { diff --git a/src/com/mostc/pftt/ui/ExpectedActualDiffPHPTDisplay.java b/src/com/mostc/pftt/ui/ExpectedActualDiffPHPTDisplay.java index b2f2d9b..abe3f7f 100644 --- a/src/com/mostc/pftt/ui/ExpectedActualDiffPHPTDisplay.java +++ b/src/com/mostc/pftt/ui/ExpectedActualDiffPHPTDisplay.java @@ -36,6 +36,7 @@ import com.mostc.pftt.util.StringUtil; import se.datadosen.component.RiverLayout; +@SuppressWarnings("serial") public class ExpectedActualDiffPHPTDisplay extends JScrollPane { protected JScrollPane horizontal_jsp, vertical_jsp; protected JPanel horizontal_panel, horizontal_button_panel, vertical_panel; @@ -44,7 +45,7 @@ public class ExpectedActualDiffPHPTDisplay extends JScrollPane { protected JTable env_table; protected DefaultListModel cmd_array_list_model; protected JList cmd_array_list; - protected JTextArea stdin_data_textarea, shell_script_textarea, expectf_textarea, pre_override_textarea; + protected JTextArea stdin_data_textarea, shell_script_textarea, expectf_textarea, pre_override_textarea, sapi_output_textarea; protected PhptTestResult test; public ExpectedActualDiffPHPTDisplay() { @@ -74,6 +75,8 @@ public class ExpectedActualDiffPHPTDisplay extends JScrollPane { expectf_textarea.setToolTipText("EXPECTF section after regular expression patterns are added"); pre_override_textarea = new JTextArea(); pre_override_textarea.setToolTipText("Actual test output before any OS specific overrides applied"); + sapi_output_textarea = new JTextArea(); + sapi_output_textarea.setToolTipText("Output from SAPI - did web server crash? etc..."); horizontal_button_panel.add(actual_display.button_panel); horizontal_button_panel.add(expected_display.button_panel); @@ -89,7 +92,9 @@ public class ExpectedActualDiffPHPTDisplay extends JScrollPane { vertical_panel.add(expectf_textarea); vertical_panel.add(pre_override_textarea); - JPanel prepared_panel = new JPanel(new GridLayout(4, 1, 2, 2)); + + JPanel prepared_panel = new JPanel(new GridLayout(5, 1, 2, 2)); + vertical_panel.add(new JScrollPane(sapi_output_textarea)); prepared_panel.add(new JScrollPane(stdin_data_textarea = new JTextArea())); stdin_data_textarea.setLineWrap(true); prepared_panel.add(new JScrollPane(shell_script_textarea = new JTextArea())); @@ -141,6 +146,10 @@ public class ExpectedActualDiffPHPTDisplay extends JScrollPane { diff_display.showFile(test.test_case, test.diff_str==null?"":test.diff_str); expectf_textarea.setText(test.expectf_output==null?"":test.expectf_output); pre_override_textarea.setText(test.preoverride_actual==null?"":test.preoverride_actual); + // + String sapi_output = test.getSAPIOutput(); + sapi_output_textarea.setText(sapi_output==null?"":sapi_output); + // SwingUtilities.invokeLater(new Runnable() { public void run() { diff --git a/src/com/mostc/pftt/ui/PhptHostTab.java b/src/com/mostc/pftt/ui/PhptHostTab.java index 2e6e7c4..e112b70 100644 --- a/src/com/mostc/pftt/ui/PhptHostTab.java +++ b/src/com/mostc/pftt/ui/PhptHostTab.java @@ -32,26 +32,28 @@ import com.mostc.pftt.runner.PhptTestPackRunner; import com.mostc.pftt.runner.AbstractTestPackRunner.ETestPackRunnerState; import com.mostc.pftt.telemetry.PhptTelemetry; import com.mostc.pftt.telemetry.PhptTestResult; +import com.mostc.pftt.util.StringUtil; import se.datadosen.component.RiverLayout; +@SuppressWarnings("serial") public class PhptHostTab extends JSplitPane { - JPanel panel, button_panel; - JProgressBar progress_bar, pass_bar; - JButton stop_button, prev_file_button, prev_button, next_file_button, next_button, to_actual_button, to_expect_button, ignore_button, skip_button, pass_button; - JLabel pass_label, total_label, fail_label, xfail_label, xfail_works_label, skip_label, xskip_label, bork_label, unsupported_label, exceptions_label; - JMenuBar jmb; - JMenu options_menu, status_list_menu; - JRadioButtonMenuItem list_fail_rb; - ExpectedActualDiffPHPTDisplay eat_display; - JCheckBoxMenuItem host_console_cb; - JSplitPane jsp; - DefaultListModel fail_list_model, xfail_list_model, xfail_works_list_model, xskip_list_model, skip_list_model, pass_list_model, bork_list_model, unsupported_list_model, exceptions_list_model; - JList test_list; - JScrollPane test_list_jsp; - ConsoleTextEditor host_console; - Host host; - JRadioButtonMenuItem list_xfail_rb, list_xfail_works_rb, list_skip_rb, list_xskip_rb, list_pass_rb, list_bork_rb, list_unsupported_rb, list_exceptions_rb; + protected JPanel panel, button_panel; + protected JProgressBar progress_bar, pass_bar; + protected JButton stop_button, prev_file_button, prev_button, next_file_button, next_button, to_actual_button, to_expect_button, ignore_button, skip_button, pass_button; + protected JLabel pass_label, total_label, fail_label, crash_label, xfail_label, xfail_works_label, skip_label, xskip_label, bork_label, unsupported_label, exceptions_label; + protected JMenuBar jmb; + protected JMenu options_menu, status_list_menu; + protected JRadioButtonMenuItem list_fail_rb; + protected ExpectedActualDiffPHPTDisplay eat_display; + protected JCheckBoxMenuItem host_console_cb; + protected JSplitPane jsp; + protected DefaultListModel fail_list_model, crash_list_model, xfail_list_model, xfail_works_list_model, xskip_list_model, skip_list_model, pass_list_model, bork_list_model, unsupported_list_model, exceptions_list_model; + protected JList test_list; + protected JScrollPane test_list_jsp; + protected ConsoleTextEditor host_console; + protected Host host; + protected JRadioButtonMenuItem list_xfail_rb, list_crash_rb, list_xfail_works_rb, list_skip_rb, list_xskip_rb, list_pass_rb, list_bork_rb, list_unsupported_rb, list_exceptions_rb; public PhptHostTab(Host host, final PhptTestPackRunner phpt_test_pack_runner) { super(JSplitPane.VERTICAL_SPLIT); @@ -63,6 +65,7 @@ public class PhptHostTab extends JSplitPane { host_console.setVisible(false); fail_list_model = new DefaultListModel(); + crash_list_model = new DefaultListModel(); xfail_list_model = new DefaultListModel(); xfail_works_list_model = new DefaultListModel(); xskip_list_model = new DefaultListModel(); @@ -108,6 +111,8 @@ public class PhptHostTab extends JSplitPane { panel.add(unsupported_label = new JLabel("0")); panel.add(new JLabel("Exceptions:")); panel.add(exceptions_label = new JLabel("0")); + panel.add(new JLabel("Crashes:")); + panel.add(crash_label = new JLabel("0")); /////////////// @@ -203,12 +208,18 @@ public class PhptHostTab extends JSplitPane { showList(unsupported_list_model); } }); list_button_group.add(list_unsupported_rb); - status_list_menu.add(list_exceptions_rb = new JRadioButtonMenuItem("Exceptions")); + status_list_menu.add(list_exceptions_rb = new JRadioButtonMenuItem("Internal Exceptions")); list_exceptions_rb.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if (list_exceptions_rb.isSelected()) showList(exceptions_list_model); } }); list_button_group.add(list_exceptions_rb); + status_list_menu.add(list_crash_rb = new JRadioButtonMenuItem("Crash")); + list_crash_rb.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { + if (list_crash_rb.isSelected()) + showList(crash_list_model); + } }); + list_button_group.add(list_crash_rb); options_menu.add(host_console_cb = new JCheckBoxMenuItem("Host Console")); host_console_cb.addActionListener(new ActionListener() { @@ -279,13 +290,21 @@ public class PhptHostTab extends JSplitPane { jsp.setDividerLocation(0.75d); } - protected int pass, fail, xfail, xfail_works, skip, xskip, bork, unsupported, exceptions; // XXX duplicates functionality from PhptTelemetry + protected int crash, pass, fail, xfail, xfail_works, skip, xskip, bork, unsupported, exceptions; // XXX duplicates functionality from PhptTelemetry public void showResult(final int total, final int completed, final PhptTestResult result) { SwingUtilities.invokeLater(new Runnable() { public void run() { progress_bar.setMaximum(total); + // + if (StringUtil.isNotEmpty(result.getSAPIOutput())) { + crash++; + crash_label.setText(Integer.toString(crash)); + crash_list_model.addElement(result); + } + // + switch(result.status) { case FAIL: fail++; diff --git a/src/com/mostc/pftt/util/StringUtil.java b/src/com/mostc/pftt/util/StringUtil.java index 08dff56..559c57e 100644 --- a/src/com/mostc/pftt/util/StringUtil.java +++ b/src/com/mostc/pftt/util/StringUtil.java @@ -204,6 +204,10 @@ public final class StringUtil { return out; } + + public static int hashCode(String a) { + return a == null ? 0 : a.hashCode(); + } private StringUtil() {} diff --git a/src/com/mostc/pftt/util/WindowsSnapshotDownloadUtil.groovy b/src/com/mostc/pftt/util/WindowsSnapshotDownloadUtil.groovy index b218869..e37d940 100644 --- a/src/com/mostc/pftt/util/WindowsSnapshotDownloadUtil.groovy +++ b/src/com/mostc/pftt/util/WindowsSnapshotDownloadUtil.groovy @@ -182,6 +182,9 @@ final class WindowsSnapshotDownloadUtil { for ( def link : links ) revisions.add(link.text()); + + // critical to getting the newest revision first + Collections.reverse(revisions); ArrayList<URL> urls = new ArrayList<URL>(revisions.size()); for ( def revision : revisions )
diff --git a/README.mdown b/README.mdown index c3300bb..29c271e 100644 --- a/README.mdown +++ b/README.mdown @@ -23,6 +23,7 @@ BSD ### Important Classes - [CliPhptTestCaseRunner](doc/apidoc/com/mostc/pftt/runner/CliPhptTestCaseRunner.html) - Executes a single PHPT Test Case (using the CLI SAPI) and evaluates + - [HttpTestCaseRunner](doc/apidoc/com/mostc/pftt/runner/HttpTestCaseRunner.html) - Executes a single PHPT Test Case against a PHP build running under a Web Server (Builtin, IIS, Apache) - [Host](doc/apidoc/com/mostc/pftt/host/Host.html) - Abstracts host management so client code doesn't need to care if host is local or remote(ssh) - [ScenarioSet](doc/apidoc/com/mostc/pftt/scenario/ScenarioSet.html) - Models 90 sets of different [Scenarios](apidoc/com/mostc/pftt/scenario/Scenario.html) to test PHP in - [PhptTestCase](doc/apidoc/com/mostc/pftt/model/phpt/PhptTestCase.html) - Models a PHPT Test case diff --git a/bin/pftt.cmd b/bin/pftt.cmd index c53c737..a8b576c 100644 --- a/bin/pftt.cmd +++ b/bin/pftt.cmd @@ -1,21 +1,21 @@ -@echo off +@ECHO off REM script for running PFTT on Windows -set PFTT_HOME=C:\php-sdk\PFTT\current -set PFTT_LIB=%PFTT_HOME%\lib +SET PFTT_HOME=C:\php-sdk\PFTT\current +SET PFTT_LIB=%PFTT_HOME%\lib -set CLASSPATH=%PFTT_HOME%\build;%PFTT_LIB%\htmlcleaner-2.2.jar;%PFTT_LIB%\groovy-1.8.6.jar;%PFTT_LIB%\icu4j-49_1.jar;%PFTT_LIB%\icudata.jar;%PFTT_LIB%\icutzdata.jar;%PFTT_LIB%\j2ssh-common-0.2.9.jar;%PFTT_LIB%\j2ssh-core-0.2.9.jar;%PFTT_LIB%\jansi-1.7.jar;%PFTT_LIB%\jline-0.9.94.jar;%PFTT_LIB%\jzlib-1.0.7.jar;%PFTT_LIB%\selenium-server-standalone-2.19.0.jar;%PFTT_LIB%\xercesImpl.jar;%PFTT_LIB%\xmlpull-1.1.3.1.jar;%PFTT_LIB%\commons-net-3.1.jar;%PFTT_LIB%\commons-cli-1.2.jar;%PFTT_LIB%\antlr-2.7.7.jar;%PFTT_LIB%\asm-3.2.jar;%PFTT_LIB%\asm-analysis-3.2.jar;%PFTT_LIB%\asm-commons-3.2.jar;%PFTT_LIB%\asm-tree-3.2.jar;%PFTT_LIB%\asm-util-3.2.jar +SET CLASSPATH=%PFTT_HOME%\build;%PFTT_LIB%\htmlcleaner-2.2.jar;%PFTT_LIB%\groovy-1.8.6.jar;%PFTT_LIB%\icu4j-49_1.jar;%PFTT_LIB%\icudata.jar;%PFTT_LIB%\icutzdata.jar;%PFTT_LIB%\j2ssh-common-0.2.9.jar;%PFTT_LIB%\j2ssh-core-0.2.9.jar;%PFTT_LIB%\jansi-1.7.jar;%PFTT_LIB%\jline-0.9.94.jar;%PFTT_LIB%\jzlib-1.0.7.jar;%PFTT_LIB%\selenium-server-standalone-2.19.0.jar;%PFTT_LIB%\xercesImpl.jar;%PFTT_LIB%\xmlpull-1.1.3.1.jar;%PFTT_LIB%\commons-net-3.1.jar;%PFTT_LIB%\commons-cli-1.2.jar;%PFTT_LIB%\antlr-2.7.7.jar;%PFTT_LIB%\asm-3.2.jar;%PFTT_LIB%\asm-analysis-3.2.jar;%PFTT_LIB%\asm-commons-3.2.jar;%PFTT_LIB%\asm-tree-3.2.jar;%PFTT_LIB%\asm-util-3.2.jar REM find java and execute PfttMain REM search %PATH% for java WHERE java > NUL -if %ERRORLEVEL% EQU 0 ( +IF %ERRORLEVEL% EQU 0 ( java -classpath %CLASSPATH% com.mostc.pftt.main.PfttMain %* ) ELSE ( REM check for JAVA_HOME - IF EXIST %JAVA_HOME%\lib\tools.jar ( + IF EXIST "%JAVA_HOME%\lib\tools.jar" ( %JAVA_HOME%\bin\java -classpath %CLASSPATH% com.mostc.pftt.main.PfttMain %* - ) ELSE ( + ) ELSE ( ECHO user error set JAVA_HOME or add java to PATH and try again ) ) diff --git a/doc/index.html b/doc/index.html index c13f472..d08d6ca 100644 --- a/doc/index.html +++ b/doc/index.html @@ -26,6 +26,7 @@ PHPUnit tests for PHP Applications, along with comparison, debugging, performanc <h2>Important Classes</h2> <p><a href="apidoc/com/mostc/pftt/runner/CliPhptTestCaseRunner.html">CliPhptTestCaseRunner</a> - Executes a single PHPT Test Case (using the CLI SAPI) and evaluates</p> +<p><a href="apidoc/com/mostc/pftt/runner/HttpTestCaseRunner.html">HttpTestCaseRunner</a> - Executes a single PHPT Test Case against a PHP build running under a Web Server (Builtin, IIS, Apache)</p> <p><a href="apidoc/com/mostc/pftt/host/Host.html">Host</a> - Abstracts host management so client code doesn't need to care if host is local or remote(ssh)</p> <p><a href="apidoc/com/mostc/pftt/scenario/ScenarioSet.html">ScenarioSet</a> - Models 90 sets of different <a href="apidoc/com/mostc/pftt/scenario/Scenario.html">Scenario</a>s to test PHP in</p> <p><a href="apidoc/com/mostc/pftt/model/phpt/PhptTestCase.html">PhptTestCase</a> - Models a PHPT Test case</p> diff --git a/hs_err_pid22876.log b/hs_err_pid22876.log new file mode 100644 index 0000000..1f8623c --- /dev/null +++ b/hs_err_pid22876.log @@ -0,0 +1,149 @@ +# +# There is insufficient memory for the Java Runtime Environment to continue. +# Native memory allocation (malloc) failed to allocate 32744 bytes for ChunkPool::allocate +# Possible reasons: +# The system is out of physical RAM or swap space +# In 32 bit mode, the process size limit was hit +# Possible solutions: +# Reduce memory load on the system +# Increase physical memory or swap space +# Check if swap backing store is full +# Use 64 bit Java on a 64 bit OS +# Decrease Java heap size (-Xmx/-Xms) +# Decrease number of Java threads +# Decrease Java thread stack sizes (-Xss) +# Set larger code cache with -XX:ReservedCodeCacheSize= +# This output file may be truncated or incomplete. +# +# Out of Memory Error (allocation.cpp:211), pid=22876, tid=8040 +# +# JRE version: 6.0_35-b10 +# Java VM: Java HotSpot(TM) 64-Bit Server VM (20.10-b01 mixed mode windows-amd64 compressed oops) + +--------------- T H R E A D --------------- + +Current thread (0x0000000005d85800): JavaThread "C2 CompilerThread1" daemon [_thread_in_native, id=8040, stack(0x0000000006d60000,0x0000000006e60000)] + +Stack: [0x0000000006d60000,0x0000000006e60000] + +Current CompileTask: +C2:1155916 9% java.util.AbstractCollection.contains(Ljava/lang/Object;)Z @ 29 (55 bytes) + + +--------------- P R O C E S S --------------- + +Java Threads: ( => current thread ) + 0x000000000724b000 JavaThread "AWT-EventQueue-0" [_thread_blocked, id=17572, stack(0x0000000009b10000,0x0000000009c10000)] + 0x0000000008283000 JavaThread "AWT-Windows" daemon [_thread_in_native, id=14188, stack(0x0000000009280000,0x0000000009380000)] + 0x00000000081a7000 JavaThread "AWT-Shutdown" [_thread_blocked, id=2360, stack(0x0000000009180000,0x0000000009280000)] + 0x0000000008658800 JavaThread "Java2D Disposer" daemon [_thread_blocked, id=27840, stack(0x0000000009080000,0x0000000009180000)] + 0x00000000071b1800 JavaThread "Timer-1" [_thread_blocked, id=12960, stack(0x0000000007560000,0x0000000007660000)] + 0x0000000007152000 JavaThread "Timer-0" [_thread_blocked, id=5892, stack(0x0000000007460000,0x0000000007560000)] + 0x0000000005da7800 JavaThread "Low Memory Detector" daemon [_thread_blocked, id=15100, stack(0x0000000006e60000,0x0000000006f60000)] +=>0x0000000005d85800 JavaThread "C2 CompilerThread1" daemon [_thread_in_native, id=8040, stack(0x0000000006d60000,0x0000000006e60000)] + 0x0000000005d6f800 JavaThread "C2 CompilerThread0" daemon [_thread_blocked, id=8600, stack(0x0000000006c60000,0x0000000006d60000)] + 0x0000000005d6c800 JavaThread "Attach Listener" daemon [_thread_blocked, id=3872, stack(0x0000000006b60000,0x0000000006c60000)] + 0x0000000005d6b800 JavaThread "Signal Dispatcher" daemon [_thread_blocked, id=13372, stack(0x0000000006a60000,0x0000000006b60000)] + 0x0000000005d1a000 JavaThread "Finalizer" daemon [_thread_blocked, id=3904, stack(0x0000000006960000,0x0000000006a60000)] + 0x0000000005d13800 JavaThread "Reference Handler" daemon [_thread_blocked, id=13252, stack(0x0000000006860000,0x0000000006960000)] + 0x000000000033b800 JavaThread "main" [_thread_in_native, id=16256, stack(0x0000000002040000,0x0000000002140000)] + +Other Threads: + 0x0000000005d08000 VMThread [stack: 0x0000000006760000,0x0000000006860000] [id=19792] + 0x0000000005db1800 WatcherThread [stack: 0x0000000006f60000,0x0000000007060000] [id=14948] + +VM state:not at safepoint (normal execution) + +VM Mutex/Monitor currently owned by a thread: None + +Heap + PSYoungGen total 164800K, used 107238K [0x00000000f5560000, 0x00000000fffa0000, 0x0000000100000000) + eden space 155648K, 66% used [0x00000000f5560000,0x00000000fb9d99c0,0x00000000fed60000) + from space 9152K, 47% used [0x00000000ff6b0000,0x00000000ffaf0000,0x00000000fffa0000) + to space 9344K, 0% used [0x00000000fed60000,0x00000000fed60000,0x00000000ff680000) + PSOldGen total 99712K, used 87086K [0x00000000e0000000, 0x00000000e6160000, 0x00000000f5560000) + object space 99712K, 87% used [0x00000000e0000000,0x00000000e550b860,0x00000000e6160000) + PSPermGen total 33664K, used 20753K [0x00000000dae00000, 0x00000000dcee0000, 0x00000000e0000000) + object space 33664K, 61% used [0x00000000dae00000,0x00000000dc2445e0,0x00000000dcee0000) + +Code Cache [0x0000000002140000, 0x00000000023b0000, 0x0000000005140000) + total_blobs=749 nmethods=292 adapters=410 free_code_cache=48972992 largest_free_block=14336 + +Dynamic libraries: +0x0000000000400000 - 0x000000000042f000 C:\Program Files\Java\jre6\bin\javaw.exe +0x00000000775e0000 - 0x000000007778b000 C:\Windows\SYSTEM32\ntdll.dll +0x00000000773c0000 - 0x00000000774df000 C:\Windows\system32\kernel32.dll +0x000007fefd8a0000 - 0x000007fefd90b000 C:\Windows\system32\KERNELBASE.dll +0x000007fefdd40000 - 0x000007fefde1b000 C:\Windows\system32\ADVAPI32.dll +0x000007fefd940000 - 0x000007fefd9df000 C:\Windows\system32\msvcrt.dll +0x000007fefe000000 - 0x000007fefe01f000 C:\Windows\SYSTEM32\sechost.dll +0x000007feff380000 - 0x000007feff4ae000 C:\Windows\system32\RPCRT4.dll +0x00000000774e0000 - 0x00000000775da000 C:\Windows\system32\USER32.dll +0x000007fefe0b0000 - 0x000007fefe117000 C:\Windows\system32\GDI32.dll +0x000007fefe020000 - 0x000007fefe02e000 C:\Windows\system32\LPK.dll +0x000007feff180000 - 0x000007feff24a000 C:\Windows\system32\USP10.dll +0x000007fefd910000 - 0x000007fefd93e000 C:\Windows\system32\IMM32.DLL +0x000007feff710000 - 0x000007feff819000 C:\Windows\system32\MSCTF.dll +0x000000006d800000 - 0x000000006dfb9000 C:\Program Files\Java\jre6\bin\server\jvm.dll +0x000007fef8d20000 - 0x000007fef8d5b000 C:\Windows\system32\WINMM.dll +0x000000006d770000 - 0x000000006d77e000 C:\Program Files\Java\jre6\bin\verify.dll +0x000000006d3b0000 - 0x000000006d3d7000 C:\Program Files\Java\jre6\bin\java.dll +0x000000006d7c0000 - 0x000000006d7d2000 C:\Program Files\Java\jre6\bin\zip.dll +0x000007fefd3d0000 - 0x000007fefd427000 C:\Windows\system32\apphelp.dll +0x000000006d000000 - 0x000000006d1c4000 C:\Program Files\Java\jre6\bin\awt.dll +0x000007fef9dd0000 - 0x000007fef9e40000 C:\Windows\system32\WINSPOOL.DRV +0x000007fefef50000 - 0x000007feff151000 C:\Windows\system32\ole32.dll +0x000007fefe1c0000 - 0x000007fefef46000 C:\Windows\system32\SHELL32.dll +0x000007feff870000 - 0x000007feff8e1000 C:\Windows\system32\SHLWAPI.dll +0x000007fefbca0000 - 0x000007fefbe94000 C:\Windows\WinSxS\amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.7600.16385_none_fa645303170382f6\COMCTL32.dll +0x000007fefb850000 - 0x000007fefb868000 C:\Windows\system32\DWMAPI.DLL +0x000007fefbc20000 - 0x000007fefbc76000 C:\Windows\system32\uxtheme.dll +0x000007fefd430000 - 0x000007fefd43f000 C:\Windows\system32\CRYPTBASE.dll +0x000000006d2a0000 - 0x000000006d307000 C:\Program Files\Java\jre6\bin\fontmanager.dll +0x000007fefe120000 - 0x000007fefe1b8000 C:\Windows\system32\COMDLG32.DLL +0x000007fef2220000 - 0x000007fef22fc000 C:\Windows\system32\spool\DRIVERS\x64\3\unidrvui.dll +0x000007fefc800000 - 0x000007fefc80c000 C:\Windows\system32\VERSION.dll +0x000007fefda40000 - 0x000007fefdb17000 C:\Windows\system32\OLEAUT32.dll +0x000007fef6b40000 - 0x000007fef6b7a000 C:\Windows\system32\spool\DRIVERS\x64\3\mxdwdui.DLL +0x000007fef2320000 - 0x000007fef23d2000 C:\Windows\system32\spool\DRIVERS\x64\3\mxdwdrv.dll +0x000000006d610000 - 0x000000006d627000 C:\Program Files\Java\jre6\bin\net.dll +0x000007feff820000 - 0x000007feff86d000 C:\Windows\system32\WS2_32.dll +0x000007fefda30000 - 0x000007fefda38000 C:\Windows\system32\NSI.dll +0x000007fefcd70000 - 0x000007fefcdc4000 C:\Windows\system32\mswsock.dll +0x000007fefcd60000 - 0x000007fefcd67000 C:\Windows\System32\wship6.dll +0x000000006d630000 - 0x000000006d63b000 C:\Program Files\Java\jre6\bin\nio.dll +0x000000006d210000 - 0x000000006d238000 C:\Program Files\Java\jre6\bin\dcpr.dll +0x000007fefc6e0000 - 0x000007fefc6e7000 C:\Windows\System32\wshtcpip.dll +0x000007fefcbf0000 - 0x000007fefcc4b000 C:\Windows\system32\DNSAPI.dll +0x000007fefc7d0000 - 0x000007fefc7f7000 C:\Windows\system32\IPHLPAPI.DLL +0x000007fefc7c0000 - 0x000007fefc7cb000 C:\Windows\system32\WINNSI.DLL +0x000007fefa240000 - 0x000007fefa248000 C:\Windows\system32\rasadhlp.dll +0x000007fefae30000 - 0x000007fefae83000 C:\Windows\System32\fwpuclnt.dll +0x00000000777b0000 - 0x00000000777b7000 C:\Windows\system32\PSAPI.DLL + +VM Arguments: +jvm_args: -Dfile.encoding=Cp1252 +java_command: com.mostc.pftt.main.PfttMain -gui phpt_all C:\php-sdk\php-5.3-nts-windows-vc9-x86-rda541ff C:\php-sdk\php-test-pack-5.3-nts-windows-vc9-x86-rda541ff +Launcher Type: SUN_STANDARD + +Environment Variables: +PATH=C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\ +USERNAME=matt +OS=Windows_NT +PROCESSOR_IDENTIFIER=Intel64 Family 6 Model 42 Stepping 7, GenuineIntel + + + +--------------- S Y S T E M --------------- + +OS: Windows 7 , 64 bit Build 7600 + +CPU:total 8 (8 cores per cpu, 1 threads per core) family 6 model 42 stepping 7, cmov, cx8, fxsr, mmx, sse, sse2, sse3, ssse3 + +Memory: 4k page, physical 2096696k(488200k free), swap 8386784k(1692k free) + +vm_info: Java HotSpot(TM) 64-Bit Server VM (20.10-b01) for windows-amd64 JRE (1.6.0_35-b10), built on Aug 28 2012 16:50:10 by "java_re" with MS VC++ 8.0 (VS2005) + +time: Wed Oct 24 13:21:55 2012 +elapsed time: 1155 seconds + diff --git a/src/com/mostc/pftt/host/ExecOutput.java b/src/com/mostc/pftt/host/ExecOutput.java index 003bba3..05f5d17 100644 --- a/src/com/mostc/pftt/host/ExecOutput.java +++ b/src/com/mostc/pftt/host/ExecOutput.java @@ -29,14 +29,14 @@ public class ExecOutput { public boolean isEmpty() { return StringUtil.isEmpty(output); } - public boolean isCrash() { + public boolean isCrashed() { return exit_code != 0; } public ExecOutput printOutputIfCrash() { return printOutputIfCrash(System.err); } public ExecOutput printOutputIfCrash(PrintStream ps) { - if (isCrash()) + if (isCrashed()) ps.println(output); return this; } diff --git a/src/com/mostc/pftt/host/Host.java b/src/com/mostc/pftt/host/Host.java index 0db68df..443bb71 100644 --- a/src/com/mostc/pftt/host/Host.java +++ b/src/com/mostc/pftt/host/Host.java @@ -400,6 +400,42 @@ public abstract class Host { public ExecOutput execElevated(String cmd, int timeout_sec, String chdir) throws Exception { return execElevated(cmd, timeout_sec, null, null, null, chdir, null, NO_TIMEOUT); } + public static abstract class ExecHandle { + public abstract void close(); + public abstract boolean isRunning(); + public abstract boolean isCrashed(); + public abstract String getOutput(); + public abstract int getExitCode(); + } + public ExecHandle execThread(String commandline) throws Exception { + return execThread(commandline, null, null, null); + } + public ExecHandle execThread(String commandline, byte[] stdin_data) throws Exception { + return execThread(commandline, null, null, stdin_data); + } + public ExecHandle execThread(String commandline, String chdir) throws Exception { + return execThread(commandline, null, chdir, null); + } + public ExecHandle execThread(String commandline, String chdir, byte[] stdin_data) throws Exception { + return execThread(commandline, null, chdir, stdin_data); + } + public ExecHandle execThread(String commandline, Map<String,String> env, byte[] stdin_data) throws Exception { + return execThread(commandline, env, null, stdin_data); + } + public ExecHandle execThread(String commandline, Map<String,String> env, String chdir) throws Exception { + return execThread(commandline, env, chdir, null); + } + /** executes command, returning immediately before it ends, returning a handle to asynchronously monitor + * the process. + * + * @param commandline + * @param env + * @param chdir + * @param stdin_data + * @return + * @throws Exception + */ + public abstract ExecHandle execThread(String commandline, Map<String,String> env, String chdir, byte[] stdin_data) throws Exception; public ExecOutput exec(String cmd, int timeout_sec, Map<String,String> object, byte[] stdin_post, Charset charset) throws IllegalStateException, Exception { return exec(cmd, timeout_sec, object, stdin_post, charset, (String)null); } @@ -524,12 +560,60 @@ public abstract class Host { // TODO } + /** gets a string of info about the host + * + * @return + * @throws Exception + */ + public String getSystemInfo() throws Exception { + if (isWindows()) + return exec("systeminfo", ONE_MINUTE).output; + else + return exec("uname -a", ONE_MINUTE).output; + } + + /** finds path to program on host + * + * @param cmd + * @return + * @throws Exception + */ + public String where(String cmd) throws Exception { + return isWindows() ? cmd("WHERE "+cmd, ONE_MINUTE).output : exec("WHICH "+cmd, ONE_MINUTE).output; + } + + /** returns true if command or program is found + * + * if false, passing this command to #exec will result in error because program can't be found + * + * @param cmd + * @return + */ public boolean hasCmd(String cmd) { try { - return isWindows() ? cmd("WHERE "+cmd, ONE_MINUTE).isNotEmpty() : exec("WHICH "+cmd, ONE_MINUTE).isNotEmpty(); + return StringUtil.isNotEmpty(where(cmd)); } catch ( Exception ex ) { return false; } } + + /** returns an address representing this host on one of the networks its on + * + * returns null if host is not on any network at all + * + * @return + */ + @Nullable + public abstract String getAddress(); + + /** servers running on Windows won't accept sockets from localhost to 127.0.0.1|localhost. + * + * instead have to use the ip address of one of its network interfaces (Windows doesn't have a Loopback(LO) network device) + * + * @return + */ + public String getLocalhostServableAddress() { + return isWindows() ? getAddress() : "127.0.0.1"; + } } // end public abstract class Host diff --git a/src/com/mostc/pftt/host/LocalHost.java b/src/com/mostc/pftt/host/LocalHost.java index f9a08e3..1b8f94b 100644 --- a/src/com/mostc/pftt/host/LocalHost.java +++ b/src/com/mostc/pftt/host/LocalHost.java @@ -8,8 +8,11 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; import java.net.UnknownHostException; import java.nio.charset.Charset; +import java.util.Enumeration; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; @@ -111,32 +114,28 @@ public class LocalHost extends Host { return exec(commandline, timeout, env, stdin, charset, chdir, null, NO_TIMEOUT); } @Override - public ExecOutput exec(String commandline, int timeout, Map<String,String> env, byte[] stdin, Charset charset, String chdir, TestPackRunnerThread thread, int thread_slow_sec) throws Exception { - HashMap<String,String> bin_env = new HashMap<String,String>(); - if (env!=null) { - for(String key : env.keySet()) { - String value = env.get(key); - bin_env.put(key, ""+value); - } - } - + public LocalExecHandle execThread(String commandline, Map<String,String> env, String chdir, byte[] stdin_data) throws Exception { + return exec_impl(splitCmdString(commandline), env, chdir, NO_TIMEOUT, stdin_data); + } + @Override + public ExecOutput exec(String commandline, int timeout, Map<String,String> env, byte[] stdin_data, Charset charset, String chdir, TestPackRunnerThread thread, int thread_slow_sec) throws Exception { ThreadSlowTask task = null; if (thread!=null && thread_slow_sec>NO_TIMEOUT) { task = new ThreadSlowTask(thread); timer.schedule(task, thread_slow_sec * 1000); } - LocalExecHandle eh = exec_impl(splitCmdString(commandline), bin_env, chdir, timeout, stdin); + LocalExecHandle eh = exec_impl(splitCmdString(commandline), env, chdir, timeout, stdin_data); - Object[] ret = eh.run_read_streams(charset); + eh.run(charset); if (task!=null) task.cancel(); ExecOutput out = new ExecOutput(); - out.output = (String) ret[0]; + out.output = eh.getOutput(); out.charset = eh.charset; - out.exit_code = eh.process.exitValue(); + out.exit_code = eh.getExitCode(); return out; } @@ -186,13 +185,14 @@ public class LocalHost extends Host { return System.getProperty("user.name"); } - public static class LocalExecHandle { + public static class LocalExecHandle extends ExecHandle { protected Process process; protected OutputStream stdin; protected InputStream stdout, stderr; protected ExitMonitorTask task; protected String cmdline; protected Charset charset; + protected StringBuilder output_sb; public LocalExecHandle(Process process, OutputStream stdin, InputStream stdout, InputStream stderr, ExitMonitorTask task, String cmdline) { this.process = process; @@ -202,7 +202,18 @@ public class LocalHost extends Host { this.task = task; this.cmdline = cmdline; } + + @Override + public boolean isRunning() { + try { + process.exitValue(); + return false; + } catch ( Exception ex ) { + return true; + } + } + @Override public void close() { for ( int i=0 ; i < 10 ; i++ ) { try { @@ -213,26 +224,25 @@ public class LocalHost extends Host { } } - public Object[] run_read_streams(Charset charset) throws IOException, InterruptedException { - String o = exec_copy_lines(stdout, charset); - String e = exec_copy_lines(stderr, null); - - int w = process.waitFor(); - - try { - process.destroy(); - } catch ( Exception ex ) { - - } - if (task!=null) - task.cancel(); - - return new Object[]{o, e, w}; + protected void run(Charset charset) throws IOException, InterruptedException { + output_sb = new StringBuilder(1024); + exec_copy_lines(output_sb, stdout, charset); + // + // ignores STDERR + //exec_copy_lines(stderr, null); + + int w = process.waitFor(); + + try { + process.destroy(); + } catch ( Exception ex ) { + + } + if (task!=null) + task.cancel(); } - protected String exec_copy_lines(InputStream in, Charset charset) throws IOException { - StringBuilder sb = new StringBuilder(4096); - + protected void exec_copy_lines(StringBuilder sb, InputStream in, Charset charset) throws IOException { DefaultCharsetDeciderDecoder d = charset == null ? null : PhptTestCase.newCharsetDeciderDecoder(); ByLineReader reader = charset == null ? new NoCharsetByLineReader(in) : new MultiCharsetByLineReader(in, d); String line; @@ -252,8 +262,26 @@ public class LocalHost extends Host { if (reader instanceof AbstractDetectingCharsetReader) this.charset = ((AbstractDetectingCharsetReader)reader).cs;// TODO d.getCommonCharset(); - - return sb.toString(); + } + + @Override + public boolean isCrashed() { + return getExitCode() != 0; + } + + @Override + public String getOutput() { + StringBuilder sb = output_sb; + return sb == null ? "" : sb.toString(); + } + + @Override + public int getExitCode() { + try { + return process.exitValue(); + } catch ( Exception ex ) { + return 0; + } } } // end public static class LocalExecHandle @@ -309,8 +337,8 @@ public class LocalHost extends Host { try { process = builder.start(); } catch ( IOException ex ) { - // randomly sometimes on Linux, get this problem ... try again if (ex.getMessage().contains("file busy")) { + // randomly sometimes on Linux, get this problem ... wait and try again Thread.sleep(100); process = builder.start(); } else { @@ -473,5 +501,33 @@ public class LocalHost extends Host { public String getOSNameLong() { return System.getProperty("os.name"); } + + @Override + public String getAddress() { + try { + Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces(); + Enumeration<InetAddress> addrs; + NetworkInterface ni; + InetAddress addr; + String addr_str; + while (interfaces.hasMoreElements()) { + ni = interfaces.nextElement(); + addrs = ni.getInetAddresses(); + while (addrs.hasMoreElements()) { + addr = addrs.nextElement(); + addr_str = addr.getHostAddress(); + if (addr_str.equals("127.0.0.1")) + continue; + if (addr_str.split("\\.").length==4) + // IPv4 address + return addr_str; + } + } + } catch (SocketException ex) { + ex.printStackTrace(); + } + // no network interfaces + return null; + } // end public String getAddress } // end public class Host diff --git a/src/com/mostc/pftt/host/SSHHost.java b/src/com/mostc/pftt/host/SSHHost.java index 6b9baee..32b9a9b 100644 --- a/src/com/mostc/pftt/host/SSHHost.java +++ b/src/com/mostc/pftt/host/SSHHost.java @@ -189,4 +189,17 @@ public class SSHHost extends RemoteHost { return null; } + @Override + public ExecHandle execThread(String commandline, Map<String, String> env, + String chdir, byte[] stdin_data) throws Exception { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getAddress() { + // TODO Auto-generated method stub + return null; + } + } diff --git a/src/com/mostc/pftt/main/PfttMain.java b/src/com/mostc/pftt/main/PfttMain.java index 2e70c86..2958c68 100644 --- a/src/com/mostc/pftt/main/PfttMain.java +++ b/src/com/mostc/pftt/main/PfttMain.java @@ -129,7 +129,7 @@ public class PfttMain { show_gui("phpt_all", gui); } - test_pack_runner.run_test_list(test_cases); + test_pack_runner.runTestList(test_cases); tmgr.close(); @@ -146,7 +146,7 @@ public class PfttMain { System.err.println(" will generate an FBC report comparing the builds"); System.err.println(); } else { - show_report(new FBCReportGen(base_telem, test_telem)); + // TODO temp show_report(new FBCReportGen(base_telem, test_telem)); } } @@ -175,7 +175,7 @@ public class PfttMain { show_gui("phpt_named", gui); } - test_pack_runner.run_test_list(test_cases); + test_pack_runner.runTestList(test_cases); tmgr.close(); diff --git a/src/com/mostc/pftt/model/phpt/EPhptTestStatus.java b/src/com/mostc/pftt/model/phpt/EPhptTestStatus.java index 6ca1afd..e90514c 100644 --- a/src/com/mostc/pftt/model/phpt/EPhptTestStatus.java +++ b/src/com/mostc/pftt/model/phpt/EPhptTestStatus.java @@ -18,5 +18,5 @@ public enum EPhptTestStatus { /** test is malformed */ BORK, /** an internal PFTT error */ - EXCEPTION + INTERNAL_EXCEPTION } diff --git a/src/com/mostc/pftt/model/phpt/PhpBuild.java b/src/com/mostc/pftt/model/phpt/PhpBuild.java index 6067bb7..d5a2b93 100644 --- a/src/com/mostc/pftt/model/phpt/PhpBuild.java +++ b/src/com/mostc/pftt/model/phpt/PhpBuild.java @@ -109,11 +109,26 @@ public class PhpBuild extends Build { if (ini!=null) return ini; } - ini = new PhpIni(host.getContents(build_path+host.dirSeparator()+"php.ini"), build_path); + String path = getDefaultPhpIniPath(host); + if (host.exists(path)) + ini = new PhpIni(host.getContents(path), build_path); + else + ini = new PhpIni(); + this.php_ini = new WeakReference<PhpIni>(ini); return ini; } + /** calculates path on Host to php.ini used for this build + * + * @param host + * @return + */ + public String getDefaultPhpIniPath(Host host) { + // XXX /etc/cli/php.ini for linux?? + return build_path+host.dirSeparator()+"php.ini"; + } + /** gets the version string for this build * * @param host @@ -166,16 +181,17 @@ public class PhpBuild extends Build { * #isExtensionEnabled checks to see if its enabled. PhpIni#hasExtension will miss builtin/static extensions. * * @param host - * @param ini * @param ext_name * @return * @throws Exception */ - public boolean isExtensionEnabled(Host host, PhpIni ini, String ext_name) throws Exception { + public boolean isExtensionEnabled(Host host, String ext_name) throws Exception { if (ext_name.equals("spl")||ext_name.equals("standard")||ext_name.equals("core")) // these extensions are always there/always builtin return true; + PhpIni ini = getDefaultPhpIni(host); + // key by only extensions, so cache will be reused even if additional directives are added if (ini!=null) ini = ini.getExtensionsOnly(); @@ -241,7 +257,7 @@ public class PhpBuild extends Build { public void setDefaultPhpIni(Host host, PhpIni ini) throws IOException { this.php_ini = new WeakReference<PhpIni>(ini); - host.saveText(build_path+"/php.ini", ini.toString()); + host.saveText(getDefaultPhpIniPath(host), ini.toString()); if (!host.isWindows()) { host.saveText("/etc/php/cli/php.ini", ini.toString()); diff --git a/src/com/mostc/pftt/model/phpt/PhpIni.java b/src/com/mostc/pftt/model/phpt/PhpIni.java index af67ddd..88dbd78 100644 --- a/src/com/mostc/pftt/model/phpt/PhpIni.java +++ b/src/com/mostc/pftt/model/phpt/PhpIni.java @@ -10,6 +10,7 @@ import javax.annotation.Nullable; import com.mostc.pftt.host.Host; import com.mostc.pftt.host.LocalHost; +import com.mostc.pftt.model.sapi.TestCaseGroupKey; import com.mostc.pftt.util.StringUtil; /** A PHP INI is the configuration for PHP. @@ -25,7 +26,7 @@ import com.mostc.pftt.util.StringUtil; * */ -public class PhpIni { +public class PhpIni extends TestCaseGroupKey { // directives public static final String INCLUDE_PATH = "include_path"; public static final String EXTENSION = "extension"; @@ -136,7 +137,7 @@ public class PhpIni { // private HashMap<String, ArrayList<String>> ini_map; private WeakReference<PhpIni> ext_ini; - private WeakReference<String> ini_str; + private WeakReference<String> ini_str, cli_arg; public PhpIni() { ini_map = new HashMap<String, ArrayList<String>>(); @@ -241,7 +242,7 @@ public class PhpIni { public void replaceAll(PhpIni ini) { this.ini_map.putAll(ini.ini_map); if (ini.countDirectives() > 0) - this.ini_str = null; + this.cli_arg = this.ini_str = null; } /** appends all values from all directives from the given PhpIni to this PhpIni @@ -291,7 +292,7 @@ public class PhpIni { ArrayList<String> values = new ArrayList<String>(1); values.add(value); ini_map.put(directive, values); - ini_str = null; + cli_arg = ini_str = null; } public void putMulti(String directive, int value) { @@ -320,7 +321,7 @@ public class PhpIni { public void remove(String directive) { ini_map.remove(directive); - ini_str = null; + cli_arg = ini_str = null; } @Nullable @@ -511,12 +512,19 @@ public class PhpIni { return ext_ini; } - /** + /** generates -d command line arguments to pass these INI directives to php.exe or php-cgi.exe * * @param host * @return */ public String toCliArgString(Host host) { + // + if (cli_arg!=null) { + String cli_arg_str = cli_arg.get(); + if (cli_arg_str!=null) + return cli_arg_str; + } + // StringBuilder sb = new StringBuilder(256); for ( String directive : getDirectives()) { String value = get(directive); @@ -537,8 +545,10 @@ public class PhpIni { sb.append(value); sb.append("\""); } - return sb.toString(); - } + String cli_arg_str = sb.toString(); + cli_arg = new WeakReference<String>(cli_arg_str); + return cli_arg_str; + } // end public String toCliArgString static final Pattern PAT_bs = Pattern.compile("\""); static final Pattern PAT_amp = Pattern.compile("\\&"); static final Pattern PAT_pipe = Pattern.compile("\\|"); diff --git a/src/com/mostc/pftt/model/phpt/PhptTestCase.java b/src/com/mostc/pftt/model/phpt/PhptTestCase.java index 9ed956a..f2a116d 100644 --- a/src/com/mostc/pftt/model/phpt/PhptTestCase.java +++ b/src/com/mostc/pftt/model/phpt/PhptTestCase.java @@ -518,8 +518,11 @@ public class PhptTestCase extends TestCase { /** returns if the string matches the name of this test * * the name should use / only not \\ + * + * test case names are normalized to only use / not \\ * * @param o + * @see #getName * @return */ public boolean isNamed(String o) { diff --git a/src/com/mostc/pftt/model/sapi/ApacheManager.java b/src/com/mostc/pftt/model/sapi/ApacheManager.java new file mode 100644 index 0000000..71a9be3 --- /dev/null +++ b/src/com/mostc/pftt/model/sapi/ApacheManager.java @@ -0,0 +1,34 @@ +package com.mostc.pftt.model.sapi; + +import javax.annotation.concurrent.ThreadSafe; + +import com.mostc.pftt.host.Host; +import com.mostc.pftt.model.phpt.PhpBuild; +import com.mostc.pftt.model.phpt.PhpIni; + +/** manages and monitors Apache HTTPD web server + * + * @author Matt Ficken + * + */ + +@ThreadSafe +public class ApacheManager extends WebServerManager { + + @Override + protected WebServerInstance createWebServerInstance(Host host, PhpBuild build, PhpIni ini, String docroot) { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean allowConcurrentWebServerSAPIInstances() { + return false; + } + + @Override + public boolean isSSLSupported() { + return true; + } + +} diff --git a/src/com/mostc/pftt/model/sapi/BuiltinWebServerInstance.java b/src/com/mostc/pftt/model/sapi/BuiltinWebServerInstance.java new file mode 100644 index 0000000..c447057 --- /dev/null +++ b/src/com/mostc/pftt/model/sapi/BuiltinWebServerInstance.java @@ -0,0 +1,46 @@ +package com.mostc.pftt.model.sapi; + +import com.mostc.pftt.host.Host.ExecHandle; +import com.mostc.pftt.model.phpt.PhpIni; + +/** running instance of PHP's builtin web server. + * + * m + * + * @author Matt Ficken + * + */ + +public class BuiltinWebServerInstance extends WebServerInstance { + protected final int port; + protected final String hostname; + protected final ExecHandle process; + + public BuiltinWebServerInstance(String[] cmd_array, PhpIni ini, ExecHandle process, String hostname, int port) { + super(cmd_array, ini); + this.process = process; + this.hostname = hostname; + this.port = port; + } + + @Override + public String hostname() { + return hostname; + } + + @Override + public int port() { + return port; + } + + @Override + public void close() { + process.close(); + } + + @Override + public boolean isRunning() { + return process.isRunning(); + } + +} // end public class BuiltinWebServerInstance diff --git a/src/com/mostc/pftt/model/sapi/BuiltinWebServerManager.java b/src/com/mostc/pftt/model/sapi/BuiltinWebServerManager.java new file mode 100644 index 0000000..e8b88a2 --- /dev/null +++ b/src/com/mostc/pftt/model/sapi/BuiltinWebServerManager.java @@ -0,0 +1,140 @@ +package com.mostc.pftt.model.sapi; + +import java.io.IOException; +import java.net.Socket; +import java.util.Timer; +import java.util.TimerTask; + +import javax.annotation.concurrent.ThreadSafe; + +import com.mostc.pftt.host.Host; +import com.mostc.pftt.host.LocalHost; +import com.mostc.pftt.model.phpt.PhpBuild; +import com.mostc.pftt.model.phpt.PhpIni; +import com.mostc.pftt.util.ErrorUtil; + +/** manages local instances of PHP's builtin web server + * + * can have multiple concurrent BuiltinWebServerInstances, assigning each one a different + * TCP port number. + * + * @author Matt Ficken + * + */ + +@ThreadSafe +public class BuiltinWebServerManager extends WebServerManager { + protected static final int PORT_RANGE_START = 40000; + // over 49151 may be used by client side of tcp sockets or other dynamic uses + protected static final int PORT_RANGE_STOP = 49151; + // + protected int last_port = PORT_RANGE_START-1; + // + protected final Timer timer; + + public BuiltinWebServerManager() { + timer = new Timer(); + } + + @Override + protected synchronized WebServerInstance createWebServerInstance(Host host, PhpBuild build, PhpIni ini, String docroot) { + String sapi_output = ""; + int port_attempts; + boolean found_port; + for (int total_attempts = 0 ; total_attempts < 3 ; total_attempts++) { + + // find port number not currently in use + port_attempts = 0; + found_port = false; + while (port_attempts < 3) { + last_port++; + if (!isLocalhostTCPPortUsed(last_port)) { + found_port = true; + break; + } else if (last_port > PORT_RANGE_STOP) { + // start over and hope some ports in range are free + last_port = PORT_RANGE_START; + port_attempts++; + } + } + + if (!found_port) { + // try again + sapi_output += "PFTT: Couldn't find unused local port\n"; + continue; + } + + // Windows BN: php -S won't accept connections if it listens on localhost|127.0.0.1 on Windows + // php won't complain about this though, it will run, but be inaccessible + String hostname = host.getLocalhostServableAddress(); + + Host.ExecHandle handle = null; + try { + // run `php.exe -S localhost:NNNN` in docroot + String cmd = build.getPhpExe()+" -S "+hostname+":"+last_port+" "+(ini==null?"":ini.toCliArgString(host)); + + handle = host.execThread(cmd, docroot); + + final Host.ExecHandle handlef = handle; + new Exception().printStackTrace(); // TODO temp + + // ensure server can be connected to + Socket sock = new Socket(hostname, last_port); + if (!sock.isConnected()) + // kill server and try again + throw new IOException("socket not connected"); + sock.close(); + // + + // provide a BuiltinWebServerInstance to handle the running web server instance + final BuiltinWebServerInstance web = new BuiltinWebServerInstance(LocalHost.splitCmdString(cmd), ini, handle, hostname, last_port); + + // check web server periodically to see if it has crashed + timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + if (!handlef.isRunning()) { + try { + if (handlef.isCrashed()) + // notify of web server crash + // + // provide output and exit code + web.notifyCrash(handlef.getOutput(), handlef.getExitCode()); + } finally { + // don't need to check any more + cancel(); + } + } + } // end public void run + }, 0, 500); + // + + return web; + } catch ( Exception ex ) { + if (handle!=null) + // make sure process is killed in this case + handle.close(); + + sapi_output += ErrorUtil.toString(ex) + "\n"; + } + } // end for + + // fallback + sapi_output += "PFTT: wasn't able to start web server instance (after many attempts)... giving up.\n"; + + // return this failure message to client code + return new CrashedWebServerInstance(ini, sapi_output); + } // end protected synchronized WebServerInstance createWebServerInstance + + @Override + public boolean allowConcurrentWebServerSAPIInstances() { + return true; + } + + @Override + public boolean isSSLSupported() { + // XXX can this web server support SSL? + return false; + } + +} // end public class BuiltinWebServerManager diff --git a/src/com/mostc/pftt/model/sapi/CrashedWebServerInstance.java b/src/com/mostc/pftt/model/sapi/CrashedWebServerInstance.java new file mode 100644 index 0000000..154ea27 --- /dev/null +++ b/src/com/mostc/pftt/model/sapi/CrashedWebServerInstance.java @@ -0,0 +1,46 @@ +package com.mostc.pftt.model.sapi; + +import com.mostc.pftt.model.phpt.PhpIni; + +/** special marker for WebServerInstances that crashed on startup/couldn't be started. + * + * special marker to just pass the crash output of the web server + * + * @see #getSAPIOutput + * @author Matt Ficken + * + */ + +public class CrashedWebServerInstance extends WebServerInstance { + protected final String sapi_output; + + public CrashedWebServerInstance(PhpIni ini, String sapi_output) { + super(null, ini); + this.sapi_output = sapi_output; + } + + @Override + public String getSAPIOutput() { + return sapi_output; + } + + @Override + public String hostname() { + return null; + } + + @Override + public int port() { + return 0; + } + + @Override + public void close() { + } + + @Override + public boolean isRunning() { + return false; + } + +} diff --git a/src/com/mostc/pftt/model/sapi/IISManager.java b/src/com/mostc/pftt/model/sapi/IISManager.java new file mode 100644 index 0000000..992d8c6 --- /dev/null +++ b/src/com/mostc/pftt/model/sapi/IISManager.java @@ -0,0 +1,34 @@ +package com.mostc.pftt.model.sapi; + +import javax.annotation.concurrent.ThreadSafe; + +import com.mostc.pftt.host.Host; +import com.mostc.pftt.model.phpt.PhpBuild; +import com.mostc.pftt.model.phpt.PhpIni; + +/** manages and monitors IIS and IIS express web servers + * + * @author Matt Ficken + * + */ + +@ThreadSafe +public class IISManager extends WebServerManager { + + @Override + protected WebServerInstance createWebServerInstance(Host host, PhpBuild build, PhpIni ini, String docroot) { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean allowConcurrentWebServerSAPIInstances() { + return false; + } + + @Override + public boolean isSSLSupported() { + return true; + } + +} diff --git a/src/com/mostc/pftt/model/sapi/SAPIInstance.java b/src/com/mostc/pftt/model/sapi/SAPIInstance.java new file mode 100644 index 0000000..06f4d5c --- /dev/null +++ b/src/com/mostc/pftt/model/sapi/SAPIInstance.java @@ -0,0 +1,13 @@ +package com.mostc.pftt.model.sapi; + +/** Running instance of a SAPI like a web server + * + * @author Matt Ficken + * + */ + +public abstract class SAPIInstance extends TestCaseGroupKey { + public abstract String getSAPIOutput(); + public abstract void close(); + public abstract boolean isRunning(); +} diff --git a/src/com/mostc/pftt/model/sapi/SAPIManager.java b/src/com/mostc/pftt/model/sapi/SAPIManager.java new file mode 100644 index 0000000..87a8ac6 --- /dev/null +++ b/src/com/mostc/pftt/model/sapi/SAPIManager.java @@ -0,0 +1,8 @@ +package com.mostc.pftt.model.sapi; + +import javax.annotation.concurrent.ThreadSafe; + +@ThreadSafe +public abstract class SAPIManager { + +} diff --git a/src/com/mostc/pftt/model/sapi/TestCaseGroupKey.java b/src/com/mostc/pftt/model/sapi/TestCaseGroupKey.java new file mode 100644 index 0000000..af7f018 --- /dev/null +++ b/src/com/mostc/pftt/model/sapi/TestCaseGroupKey.java @@ -0,0 +1,6 @@ +package com.mostc.pftt.model.sapi; + +public abstract class TestCaseGroupKey { + public abstract boolean equals(Object o); + public abstract int hashCode(); +} diff --git a/src/com/mostc/pftt/model/sapi/WebServerInstance.java b/src/com/mostc/pftt/model/sapi/WebServerInstance.java new file mode 100644 index 0000000..d1ee66f --- /dev/null +++ b/src/com/mostc/pftt/model/sapi/WebServerInstance.java @@ -0,0 +1,154 @@ +package com.mostc.pftt.model.sapi; + +import java.util.LinkedList; + +import javax.annotation.concurrent.ThreadSafe; + +import com.mostc.pftt.model.phpt.PhpIni; +import com.mostc.pftt.model.phpt.PhptTestCase; +import com.mostc.pftt.util.StringUtil; + +/** an instance of a web server + * + * @see WebServerManager + * @author Matt Ficken + * + */ + +@ThreadSafe +public abstract class WebServerInstance extends SAPIInstance { + protected LinkedList<PhptTestCase> active_test_cases; + private boolean crashed = false; + private String sapi_output = ""; + private Object sync_lock = new Object(); + protected final PhpIni ini; + protected final String[] cmd_array; + WebServerInstance replacement; // @see WebServerManager#getWebServerInstance + + public WebServerInstance(String[] cmd_array, PhpIni ini) { + this.cmd_array = cmd_array; + this.ini = ini; + active_test_cases = new LinkedList<PhptTestCase>(); + } + + @Override + public int hashCode() { + return this.port() + StringUtil.hashCode(this.hostname()); + } + + @Override + public boolean equals(Object o) { + if (o==this) + return true; + if (o instanceof WebServerInstance) { + WebServerInstance wsio = (WebServerInstance) o; + return wsio.port() == this.port() && StringUtil.equalsIC(wsio.hostname(), this.hostname()); + } + return false; + } + + @Override + public String getSAPIOutput() { + synchronized(sync_lock) { + return sapi_output; + } + } + + /** handles reporting an instance that crashed + * + * @param output - any output server returned when it crashed (or null) + * @param exit_code - exit code that was returned + */ + public void notifyCrash(String output, int exit_code) { + synchronized(sync_lock) { + // + if (crashed) { + if (StringUtil.isNotEmpty(output)) { + // shouldn't happen, but capture this output if it does + StringBuilder sb = new StringBuilder(); + if (sapi_output!=null) + sb.append(sapi_output); + sb.append("PFTT: later web server returned exit code("+exit_code+") and output:\n"); + sb.append(output); + sapi_output = sb.toString(); + } + return; + } + // + + // if crash, record output with all tests that were running during crash + crashed = true; + + StringBuilder sb = new StringBuilder(1024); + + sb.append("PFTT: web server crashed with exit code: "+exit_code); + synchronized(active_test_cases) { + sb.append("PFTT: while running these tests("+active_test_cases.size()+"):\n"); + for (PhptTestCase test_case : active_test_cases ) { + sb.append("PFTT: "); + sb.append(test_case.getName()); + sb.append('\n'); + } + } + if (StringUtil.isEmpty(output)) { + sb.append("PFTT: web server returned no output when it exited.\n"); + } else { + sb.append("PFTT: before crashing/exiting web server returned("+output.length()+"):\n"); + sb.append(output); + } + + sapi_output = sb.toString(); + } // end sync + } // end protected void notifyCrash + + /** called before HTTP request made to server for given test_case + * + * @param test_case + */ + public void notifyTestPreRequest(PhptTestCase test_case) { + synchronized(active_test_cases) { + active_test_cases.add(test_case); + } + } + + /** called immediately after HTTP response from server for the test case. + * + * the sooner this is called the better, to minimize getting other test cases that were + * executed after this one (which wouldn't be related to a crash this test case caused). + * + * @param test_case + */ + public void notifyTestPostResponse(PhptTestCase test_case) { + synchronized(active_test_cases) { + active_test_cases.remove(test_case); + } + if (replacement!=null) + replacement.notifyTestPostResponse(test_case); + } + + /** returns TRUE if this web server crashed + * + * @return + */ + public boolean isCrashed() { + synchronized(sync_lock) { + return crashed; + } + } + + public abstract String hostname(); + public abstract int port(); + + /** PhpIni set for this web server + * + * @return + */ + public PhpIni getPhpIni() { + return ini; + } + + public String[] getCmdString() { + return cmd_array; + } + +} // end public abstract class WebServerInstance diff --git a/src/com/mostc/pftt/model/sapi/WebServerManager.java b/src/com/mostc/pftt/model/sapi/WebServerManager.java new file mode 100644 index 0000000..195c57c --- /dev/null +++ b/src/com/mostc/pftt/model/sapi/WebServerManager.java @@ -0,0 +1,118 @@ +package com.mostc.pftt.model.sapi; + +import java.io.IOException; +import java.net.Socket; +import java.util.Iterator; +import java.util.LinkedList; + +import javax.annotation.concurrent.ThreadSafe; + +import com.mostc.pftt.host.Host; +import com.mostc.pftt.model.phpt.PhpBuild; +import com.mostc.pftt.model.phpt.PhpIni; + +/** Manages a certain type of web server, such as PHP's builtin web server. + * + * @author Matt Ficken + * + */ + +@ThreadSafe +public abstract class WebServerManager extends SAPIManager { + public static final String LOCALHOST = "localhost"; + // + protected final LinkedList<WebServerInstance> instances; + + public WebServerManager() { + instances = new LinkedList<WebServerInstance>(); + } + + /** closes all web servers this managed + * + */ + public void close() { + synchronized(instances) { + Iterator<WebServerInstance> it = instances.iterator(); + while (it.hasNext()) { + it.next().close(); + it.remove(); + } + } + } + + /** gets a running WebServerInstance + * + * if given an existing assigned WebServerInstance that hasn't crashed and is running, + * returns that WebServerInstance for additional use. + * + * otherwise, if no WebServerInstance or if it crashed, creates a new WebServerInstance and returns it + * + * if the same crashed WebServerInstance is provided to this several times, doesn't create replacement for each, + * uses the first replacement (to not have crashed). + * + * @param host + * @param build + * @param ini + * @param docroot + * @param assigned + * @return + */ + public WebServerInstance getWebServerInstance(Host host, PhpBuild build, PhpIni ini, String docroot, WebServerInstance assigned) { + WebServerInstance sapi; + if (assigned!=null) { + if (!assigned.isCrashed()) + return assigned; + synchronized(assigned) { + for (WebServerInstance c=assigned.replacement ; c != null ; c = c.replacement) { + synchronized(c) { + if (!c.isCrashed()) + return c; + } + } + } + assigned.replacement = sapi = createWebServerInstance(host, build, ini, docroot); + synchronized(assigned.active_test_cases) { + sapi.active_test_cases.addAll(assigned.active_test_cases); + } + } else { + sapi = createWebServerInstance(host, build, ini, docroot); + } + if (!sapi.isCrashed()) { + synchronized(instances) { + instances.add(sapi); + } + } + return sapi; + } + + protected abstract WebServerInstance createWebServerInstance(Host host, PhpBuild build, PhpIni ini, String docroot); + + /** some web servers can only have one active instance at any one time + * + * @see WebServerInstance#close + * @see WebServerInstance#isRunning + * @return + */ + public abstract boolean allowConcurrentWebServerSAPIInstances(); + + public abstract boolean isSSLSupported(); + + /** checks if the given port is being used on localhost + * + * @param port + */ + public static final boolean isLocalhostTCPPortUsed(int port) { + return isTCPPortUsed(LOCALHOST, port); + } + + public static final boolean isTCPPortUsed(String hostname, int port) { + try { + Socket sock = new Socket(hostname, port); + + return sock.isConnected(); + } catch ( IOException ex ) { + return false; + } + } + +} // end public abstract class WebServerManager diff --git a/src/com/mostc/pftt/model/sapi/package.html b/src/com/mostc/pftt/model/sapi/package.html new file mode 100644 index 0000000..2b4b5fd --- /dev/null +++ b/src/com/mostc/pftt/model/sapi/package.html @@ -0,0 +1,7 @@ +<html> + <body> + <p> + Model and management of PHP SAPIs (CLI and web servers) + </p> + </body> +</html> \ No newline at end of file diff --git a/src/com/mostc/pftt/runner/AbstractPhptTestCaseRunner.java b/src/com/mostc/pftt/runner/AbstractPhptTestCaseRunner.java index 2d3bf13..cd55764 100644 --- a/src/com/mostc/pftt/runner/AbstractPhptTestCaseRunner.java +++ b/src/com/mostc/pftt/runner/AbstractPhptTestCaseRunner.java @@ -2,6 +2,15 @@ package com.mostc.pftt.runner; import java.io.IOException; +import com.mostc.pftt.host.Host; +import com.mostc.pftt.model.phpt.EPhptTestStatus; +import com.mostc.pftt.model.phpt.PhpBuild; +import com.mostc.pftt.model.phpt.PhpIni; +import com.mostc.pftt.model.phpt.PhptTestPack; +import com.mostc.pftt.model.phpt.PhptTestCase; +import com.mostc.pftt.telemetry.PhptTelemetryWriter; +import com.mostc.pftt.telemetry.PhptTestResult; + public abstract class AbstractPhptTestCaseRunner { public static final String ENV_SCRIPT_FILENAME = "SCRIPT_FILENAME"; public static final String ENV_PATH_TRANSLATED = "PATH_TRANSLATED"; @@ -17,4 +26,43 @@ public abstract class AbstractPhptTestCaseRunner { public static final String ENV_HTTP_CONTENT_ENCODING = "HTTP_CONTENT_ENCODING"; public abstract void runTest() throws IOException, Exception, Throwable; + + public static PhpIni createIniForTest(Host host, PhpBuild build, PhptTestPack test_pack, PhptTestCase test_case) { + PhpIni ini = PhpIni.createDefaultIniCopy(host); + ini.replaceAll(test_case.getINI(test_pack, host)); + ini.addToIncludePath(host, test_pack.getTestPack()); + return ini; + } + + /** TODO + * + * @param twriter + * @param host + * @param build + * @param test_case + * @return + * @throws Exception + */ + public static boolean willSkip(PhptTelemetryWriter twriter, Host host, PhpBuild build, PhptTestCase test_case) throws Exception { + if (!host.isWindows() && test_case.getName().contains("-win32")) { + // skip windows specific tests if host is not windows + + twriter.addResult(new PhptTestResult(host, EPhptTestStatus.XSKIP, test_case, "OS not supported", null, null, null, null, null, null, null, null, null, null)); + + return true; + } else if (test_case.isNamed("ext/standard/tests/php_ini_loaded_file.phpt")||test_case.isNamed("tests/run-test/test010.phpt")||test_case.isNamed("ext/standard/tests/misc/time_sleep_until_basic.phpt") || test_case.getName().contains("session") || test_case.isNamed("ext/standard/tests/misc/time_nanosleep_basic.phpt")) { + twriter.addResult(new PhptTestResult(host, EPhptTestStatus.XSKIP, test_case, "test sometimes randomly fails, ignore it", null, null, null, null, null, null, null, null, null, null)); + + return true; + } else if (test_case.isExtensionTest() && !build.isExtensionEnabled(host, test_case.getExtensionName())) { + // if extension-under-test is not loaded, don't bother running test since it'll just be skipped (or false fail) + + twriter.addResult(new PhptTestResult(host, EPhptTestStatus.SKIP, test_case, "Extension not loaded", null, null, null, null, null, null, null, null, null, null)); + + return true; + } + + + return false; + } // end public static boolean willSkip } diff --git a/src/com/mostc/pftt/runner/AbstractPhptTestCaseRunner2.java b/src/com/mostc/pftt/runner/AbstractPhptTestCaseRunner2.java index df8f725..df4e987 100644 --- a/src/com/mostc/pftt/runner/AbstractPhptTestCaseRunner2.java +++ b/src/com/mostc/pftt/runner/AbstractPhptTestCaseRunner2.java @@ -2,11 +2,8 @@ package com.mostc.pftt.runner; import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileOutputStream; -import java.io.FileWriter; import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; +import java.nio.charset.Charset; import java.util.HashMap; import java.util.regex.Pattern; import java.util.zip.Deflater; @@ -14,9 +11,7 @@ import java.util.zip.DeflaterOutputStream; import org.incava.util.diff.Diff; -import com.mostc.pftt.host.ExecOutput; import com.mostc.pftt.host.Host; -import com.mostc.pftt.host.LocalHost; import com.mostc.pftt.model.phpt.EPhptSection; import com.mostc.pftt.model.phpt.EPhptTestStatus; import com.mostc.pftt.model.phpt.PhpBuild; @@ -36,7 +31,8 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu protected final Host host; protected final PhpBuild build; protected final PhptTestPack test_pack; - protected final PhptTestCase test_case; + protected final PhptTestCase test_case; + // TODO shift all ENV handling to CliPhptTestCaseRunner protected HashMap<String,String> env; protected final ScenarioSet scenario_set; @@ -52,53 +48,46 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu protected String temp_dir, test_dir; protected String main_file_name; protected String skip_cmd; - protected ExecOutput output; protected String test_file; protected String test_skipif; protected String test_clean; - protected String tmp_post; - protected PhpIni ini; - + protected String tmp_post, content_type; + protected final PhpIni ini; + + /** runs the test case and reports the results to the PhptTelemetryManager + * + * @see #willSkip - called by PhptTestPackRunner before #runTest is called + * + */ @Override public void runTest() throws IOException, Exception, Throwable { - // Default ini settings - ini = PhpIni.createDefaultIniCopy(host); +// ini = PhpIni.createDefaultIniCopy(host); if (!prepare()) return; - - if (!host.isWindows() && test_case.getName().contains("-win32")) { - // skip windows specific tests if host is not windows - - twriter.addResult(new PhptTestResult(host, EPhptTestStatus.XSKIP, test_case, "OS not supported", null, null, null, null, null, null, null, null, null, null)); - - return; - } else if (test_case.isNamed("ext/standard/tests/php_ini_loaded_file.phpt")||test_case.isNamed("tests/run-test/test010.phpt")||test_case.isNamed("ext/standard/tests/misc/time_sleep_until_basic.phpt") || test_case.getName().contains("session") || test_case.isNamed("ext/standard/tests/misc/time_nanosleep_basic.phpt")) { - twriter.addResult(new PhptTestResult(host, EPhptTestStatus.XSKIP, test_case, "test sometimes randomly fails, ignore it", null, null, null, null, null, null, null, null, null, null)); - - return; - } - // #prepare() loads --INI-- section from PHPT - if (test_case.isExtensionTest() && !build.isExtensionEnabled(host, ini, test_case.getExtensionName())) { - // if extension-under-test is not loaded, don't bother running test since it'll just be skipped (or false fail) - - twriter.addResult(new PhptTestResult(host, EPhptTestStatus.SKIP, test_case, "Extension not loaded", null, null, null, null, null, null, null, null, null, null)); - - return; - } - - this.executeSkipIf(); // XXX check if prepare() has borked the test -> don't bother running SKIPIF leads to test_skipif=null -> exception - if (!evalSkipIf()) { - this.prepareTest(); - this.executeTest(); - this.evalTest(); - this.executeClean(); + + notifyStart(); + + try { + if (!evalSkipIf(executeSkipIf())) { + prepareTest(); + String test_output = executeTest(); + notifyEnd(); // do ASAP + evalTest(test_output, null); // TODO null + executeClean(); + } + } finally { + // ok to call twice + notifyEnd(); } } + protected void notifyStart() {} + protected void notifyEnd() {} - public AbstractPhptTestCaseRunner2(PhptThread thread, PhptTestCase test_case, PhptTelemetryWriter twriter, Host host, ScenarioSet scenario_set, PhpBuild build, PhptTestPack test_pack) { + public AbstractPhptTestCaseRunner2(PhpIni ini, PhptThread thread, PhptTestCase test_case, PhptTelemetryWriter twriter, Host host, ScenarioSet scenario_set, PhpBuild build, PhptTestPack test_pack) { + this.ini = ini; this.thread = thread; this.test_case = test_case; this.twriter = twriter; @@ -110,9 +99,14 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu env = new HashMap<String,String>(); } - public boolean prepare() throws IOException, Exception { - //tested_file = test_case.name; - + /** prepares to execute the test case up to executing the SKIPIF section + * + * @see #prepareTest + * @return FALSE - if preparation fails so test can't be executed + * @throws IOException + * @throws Exception + */ + protected boolean prepare() throws IOException, Exception { if (test_case.hasBorkInfo()) { twriter.addResult(new PhptTestResult(host, EPhptTestStatus.BORK, test_case, test_case.getBorkInfo(), null, null, null, null, null, null, null, null, null, null)); @@ -125,11 +119,10 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu return false; } - //tested = test_case.getTrim(EPHPTSection.TEST); - selected_php_exe = build.getPhpExe(); /* For GET/POST tests, check if cgi sapi is available and if it is, use it. */ + // TODO php-cgi not required for testing with web server - can't xskip if (test_case.containsAnySection(EPhptSection.REQUEST, EPhptSection.GET, EPhptSection.POST, EPhptSection.PUT, EPhptSection.POST_RAW, EPhptSection.COOKIE, EPhptSection.EXPECTHEADERS)) { if (build.hasPhpCgiExe()) { selected_php_exe = build.getPhpCgiExe() + " -C "; @@ -185,13 +178,11 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu // Any special ini settings // these may overwrite the test defaults... - if (test_case.containsSection(EPhptSection.INI)) { - ini.replaceAll(test_case.getINI(test_pack, host)); - } - - // ask the scenario to prepare for the test (configure for database, file server, etc...) +// if (test_case.containsSection(EPhptSection.INI)) { +// ini.replaceAll(test_case.getINI(test_pack, host)); +// } + // TODO scenario.prepare(null, test_file, env, ini_map); - // TODO scenario_set.prepare(); // read ENV vars from test, from its parent (if a test redirected to this test), and merge from scenario env = test_case.getENV(env, host, build); @@ -201,11 +192,6 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu if (build.hasPhpCgiExe()) env.put(ENV_TEST_PHP_CGI_EXECUTABLE, build.getPhpCgiExe()); - - // setup the include path in the INI Configuration - ini.addToIncludePath(host, Host.dirname(test_case.getName())); - ini.addToIncludePath(host, test_pack.getTestPack()); // TODO ? - // ini_settings = ini.toCliArgString(host); @@ -213,14 +199,26 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu } // end boolean prepare static final Pattern PAT_bs = Pattern.compile("\""); - public abstract void executeSkipIf() throws Exception; - - public boolean evalSkipIf() throws IOException { + /** executes SKIPIF section and returns output + * + * @return + * @throws Exception + */ + protected abstract String executeSkipIf() throws Exception; + + /** evaluates the execution output of the SKIPIF section to decide if test should be + * skipped. reports result to PhptTelemetryManager. + * + * @param output + * @return FALSE - skip test + * @throws IOException + */ + public boolean evalSkipIf(String output) throws IOException { if (skip_cmd==null) // execute this test, don't skip it return false; - String lc_output = output.output.toLowerCase(); + String lc_output = output.toLowerCase(); if (lc_output.contains("skip")) { // test is to be skipped @@ -228,15 +226,15 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu if (host.isWindows()) { if ( (lc_output.contains("only")&&lc_output.contains("linux"))||(lc_output.contains("not")&&lc_output.contains("windows"))) // can"t run this test on this OS - twriter.addResult(new PhptTestResult(host, EPhptTestStatus.XSKIP, test_case, output.output, null, null, null, null, null, null, null, null, null, null)); + twriter.addResult(new PhptTestResult(host, EPhptTestStatus.XSKIP, test_case, output, null, null, null, null, null, null, null, null, null, null)); else - twriter.addResult(new PhptTestResult(host, EPhptTestStatus.SKIP, test_case, output.output, null, null, null, null, null, null, null, null, null, null)); + twriter.addResult(new PhptTestResult(host, EPhptTestStatus.SKIP, test_case, output, null, null, null, null, null, null, null, null, null, null)); } else { if ( (lc_output.contains("only")&&lc_output.contains("windows"))||(lc_output.contains("not")&&lc_output.contains("linux"))) // can"t run this test on this OS - twriter.addResult(new PhptTestResult(host, EPhptTestStatus.XSKIP, test_case, output.output, null, null, null, null, null, null, null, null, null, null)); + twriter.addResult(new PhptTestResult(host, EPhptTestStatus.XSKIP, test_case, output, null, null, null, null, null, null, null, null, null, null)); else - twriter.addResult(new PhptTestResult(host, EPhptTestStatus.SKIP, test_case, output.output, null, null, null, null, null, null, null, null, null, null)); + twriter.addResult(new PhptTestResult(host, EPhptTestStatus.SKIP, test_case, output, null, null, null, null, null, null, null, null, null, null)); } // skip this test @@ -248,7 +246,13 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu } // end void evalSkipIf static final Pattern PATTERN_CONTENT_TYPE = Pattern.compile("Content-Type:(.*)"); - public void prepareTest() throws Exception { + /** prepares to execute the test after the SKIPIF section is executed (if any) + * #prepare prepares only up to that, this does the rest. + * to avoid doing a full preparation for tests that will just be skipped. + * + * @throws Exception + */ + protected void prepareTest() throws Exception { if (test_case.containsSection(EPhptSection.FILE_EXTERNAL)) { // open external file and copy to test_file (binary, no char conversion - which could break it - often this is a PHAR file - which will be broken if charset coversion is done) host.copyFile(test_pack.getTestPack()+host.dirSeparator()+Host.dirname(test_case.getName()) + "/" + test_case.get(EPhptSection.FILE_EXTERNAL), test_file); @@ -297,16 +301,16 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu // // content type should look like this (boundary may differ): // "multipart/form-data; boundary=---------------------------20896060251896012921717172737" - String ct = line.substring("Content-Type: ".length()); + content_type = line.substring("Content-Type: ".length()); - env.put(ENV_CONTENT_TYPE, ct); + env.put(ENV_CONTENT_TYPE, content_type); first_ct = false; } else if (first_ct) { // content type may look like this: // "multipart/form-data" or "application/x-www-urlencoded" - String ct = line.substring("Content-Type: ".length()); + content_type = line.substring("Content-Type: ".length()); - env.put(ENV_CONTENT_TYPE, ct); + env.put(ENV_CONTENT_TYPE, content_type); first_ct = false; } else { request_sb.append(line); @@ -349,7 +353,7 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu if (StringUtil.isEmpty(env.get(ENV_CONTENT_TYPE))) { String[] res = StringUtil.getMatches(PATTERN_CONTENT_TYPE, line, twriter); if (StringUtil.isNotEmpty(res)) { - env.put(ENV_CONTENT_TYPE, res[1].trim()); + env.put(ENV_CONTENT_TYPE, content_type = res[1].trim()); continue; } } @@ -404,7 +408,7 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu env.put(ENV_REQUEST_METHOD, "POST"); if (!env.containsKey(ENV_CONTENT_TYPE)) - env.put(ENV_CONTENT_TYPE, "application/x-www-form-urlencoded"); + env.put(ENV_CONTENT_TYPE, content_type = "application/x-www-form-urlencoded"); // critical: php-cgi won"t read more bytes than this (thus some input can go missing) env.put(ENV_CONTENT_LENGTH, ""+content_length); @@ -424,65 +428,9 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu env.put(ENV_USE_ZEND_ALLOC, "1"); // prepare STDIN - String stdin_file = test_file + ".stdin"; - if (stdin_file.startsWith(test_pack.getTestPack())) { - stdin_file = stdin_file.substring(test_pack.getTestPack().length()); - if (stdin_file.startsWith("/")||stdin_file.startsWith("\\")) - stdin_file = stdin_file.substring(1); - stdin_file = twriter.telem_dir+"/"+stdin_file; - } - new File(stdin_file).getParentFile().mkdirs(); - if (stdin_post!=null) { - FileOutputStream fw = new FileOutputStream(stdin_file); - fw.write(stdin_post); - fw.close(); - } + prepareSTDIN(); - // useful: rm -rf `find ./ -name "*.sh"` - // - // create a .cmd (Windows batch script) or .sh (shell script) that will actually execute PHP - // this enables PHP to be executed like what PFTT does, but using a shell|batch script - shell_file = test_file + (host.isWindows() ? ".cmd" : ".sh" ); - if (shell_file.startsWith(test_pack.getTestPack())) { - shell_file = shell_file.substring(test_pack.getTestPack().length()); - if (shell_file.startsWith("/")||shell_file.startsWith("\\")) - shell_file = shell_file.substring(1); - shell_file = twriter.telem_dir+"/"+shell_file; - } - StringWriter sw = new StringWriter(); - PrintWriter fw = new PrintWriter(sw); - if (host.isWindows()) { - fw.println("@echo off"); // critical: or output will include these commands and fail test - } else { - fw.println("#!/bin/sh"); - } - fw.println("cd "+host.fixPath(test_dir)); - for ( String name : env.keySet()) { - String value = env.get(name); - if (value==null) - value = ""; - else - value = StringUtil.replaceAll(PAT_bs, "\\\\\"", StringUtil.replaceAll(PAT_dollar, "\\\\\\$", value)); - - if (host.isWindows()) { - if (value.contains(" ")) - // important: on Windows, don't use " " for script variables unless you have to - // the " " get passed into PHP so variables won't be read correctly - fw.println("set "+name+"=\""+value+"\""); - else - fw.println("set "+name+"="+value+""); - } else - fw.println("export "+name+"=\""+value+"\""); - } - fw.println(cmd); - fw.close(); - shell_script = sw.toString(); - FileWriter w = new FileWriter(shell_file); - fw = new PrintWriter(w); - fw.print(shell_script); - fw.flush(); - fw.close(); - w.close(); + createShellScript(); Thread.yield(); if (!host.isWindows()) { @@ -494,21 +442,48 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu } // end void prepareTest static final Pattern PAT_dollar = Pattern.compile("\\$"); - public abstract void executeTest() throws Exception; - - public abstract void executeClean() throws Exception; + protected abstract void createShellScript() throws IOException; + protected abstract void prepareSTDIN() throws IOException; + + /** executes the test (the TEST section of PhptTestCase) and returns the actual output + * + * @return + * @throws Exception + */ + protected abstract String executeTest() throws Exception; + + /** executes CLEAN section of test to cleanup after execution + * + * @throws Exception + */ + protected abstract void executeClean() throws Exception; + + /** returns output from SAPI (ex: Web Server) used to run the test, + * if it crashed. if SAPI did not crash, returns null. + * + * used to record crash output of a web server along with test result for + * later analysis. + * + * @see WebserverInstance#getSAPIOutput + * @return + */ + protected abstract String getCrashedSAPIOutput(); private boolean check() { return !test_case.isNamed("ext/phar/tests/tar/phar_setsignaturealgo2.phpt"); } - public void evalTest() throws Throwable { - if (output.exit_code!=0 && output.output.trim().length()==0) { - output.output = "PFTT: error: maybe crash?: php.exe exited with non-zero code: " + output.exit_code; - } + /** evaluates the output of the executed test and reports the result + * + * @param output + * @param charset + * @throws Throwable + */ + public void evalTest(String output, Charset charset) throws Throwable { + String output_trim = output.trim(); // line endings are already made consistent by Host#exec - String expected, output_trim, preoverride_actual = null; + String expected, preoverride_actual = null; if (test_case.containsSection(EPhptSection.EXPECTF) || test_case.containsSection(EPhptSection.EXPECTREGEX)) { @@ -520,8 +495,8 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu boolean expected_re_match; - output.output = remove_header_from_output(output.output); - output_trim = output.output.trim(); + output = remove_header_from_output(output); + output_trim = output.trim(); try { expected_re_match = test_case.getExpectedCompiled(host, twriter).match(output_trim); @@ -531,19 +506,19 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu } if (expected_re_match||check()) { - twriter.addResult(new PhptTestResult(host, test_case.isXFail()?EPhptTestStatus.XFAIL:EPhptTestStatus.PASS, test_case, output.output, null, null, output.charset, env, LocalHost.splitCmdString(cmd), stdin_post, shell_script, null, null, null)); + twriter.addResult(new PhptTestResult(host, test_case.isXFail()?EPhptTestStatus.XFAIL:EPhptTestStatus.PASS, test_case, output, null, null, charset, env, splitCmdString(), stdin_post, shell_script, null, null, null)); return; } } else if (test_case.containsSection(EPhptSection.EXPECT)) { expected = test_case.getTrim(EPhptSection.EXPECT); - output.output = remove_header_from_output(output.output); - output_trim = output.output.trim(); + output = remove_header_from_output(output); + output_trim = output.trim(); if (output_trim.equals(expected)||output_trim.contains(expected)||expected.contains(output_trim)||check()) { - twriter.addResult(new PhptTestResult(host, test_case.isXFail()?EPhptTestStatus.XFAIL:EPhptTestStatus.PASS, test_case, output.output, null, null, output.charset, env, LocalHost.splitCmdString(cmd), stdin_post, shell_script, null, null, null)); + twriter.addResult(new PhptTestResult(host, test_case.isXFail()?EPhptTestStatus.XFAIL:EPhptTestStatus.PASS, test_case, output, null, null, charset, env, splitCmdString(), stdin_post, shell_script, null, null, null)); return; } @@ -555,17 +530,17 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu // compare again if (output_trim.equals(expected)||output_trim.contains(expected)||expected.contains(output_trim)||(output_trim.length()>20&&expected.length()>20&&output_trim.substring(10, 20).equals(expected.substring(10, 20)))) { - twriter.addResult(new PhptTestResult(host, test_case.isXFail()?EPhptTestStatus.XFAIL:EPhptTestStatus.PASS, test_case, output.output, null, null, output.charset, env, LocalHost.splitCmdString(cmd), stdin_post, shell_script, null, null, null)); + twriter.addResult(new PhptTestResult(host, test_case.isXFail()?EPhptTestStatus.XFAIL:EPhptTestStatus.PASS, test_case, output, null, null, charset, env, splitCmdString(), stdin_post, shell_script, null, null, null)); return; } // end if } } else if (test_case.containsSection(EPhptSection.EXPECTHEADERS)) { - output.output = remove_header_from_output(output.output); - output_trim = output.output.trim(); + output = remove_header_from_output(output); + output_trim = output.trim(); if (StringUtil.isEmpty(output_trim)) { - twriter.addResult(new PhptTestResult(host, test_case.isXFail()?EPhptTestStatus.XFAIL:EPhptTestStatus.PASS, test_case, output.output, null, null, output.charset, env, LocalHost.splitCmdString(cmd), stdin_post, shell_script, null, null, null)); + twriter.addResult(new PhptTestResult(host, test_case.isXFail()?EPhptTestStatus.XFAIL:EPhptTestStatus.PASS, test_case, output, null, null, charset, env, splitCmdString(), stdin_post, shell_script, null, null, null)); return; } @@ -574,12 +549,12 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu // if here, test failed! if (test_case.isXFail()) { - twriter.addResult(new PhptTestResult(host, EPhptTestStatus.XFAIL, test_case, output.output, null, null, output.charset, env, LocalHost.splitCmdString(cmd), stdin_post, shell_script, null, null, preoverride_actual)); + twriter.addResult(new PhptTestResult(host, EPhptTestStatus.XFAIL, test_case, output, null, null, charset, env, splitCmdString(), stdin_post, shell_script, null, null, preoverride_actual)); } else { // test is FAIL // generate a diff - String[] actual_lines = StringUtil.splitLines(output.output.trim()); + String[] actual_lines = StringUtil.splitLines(output_trim); String[] expected_lines = StringUtil.splitEqualsSign(test_case.getExpected()); Diff<String> diff = new Diff<String>(actual_lines, expected_lines); @@ -591,10 +566,12 @@ public abstract class AbstractPhptTestCaseRunner2 extends AbstractPhptTestCaseRu expectf = null; } - twriter.addResult(new PhptTestResult(host, EPhptTestStatus.FAIL, test_case, output.output, actual_lines, expected_lines, output.charset, env, LocalHost.splitCmdString(cmd), stdin_post, shell_script, diff, expectf, preoverride_actual)); + twriter.addResult(new PhptTestResult(host, EPhptTestStatus.FAIL, test_case, output, actual_lines, expected_lines, charset, env, splitCmdString(), stdin_post, shell_script, diff, expectf, preoverride_actual, getCrashedSAPIOutput())); } } // end void evalTest + protected abstract String[] splitCmdString(); + protected static String remove_header_from_output(String output) { if (!output.contains("X-Power")) return output; diff --git a/src/com/mostc/pftt/runner/CliPhptTestCaseRunner.java b/src/com/mostc/pftt/runner/CliPhptTestCaseRunner.java index 9d47023..e906b7a 100644 --- a/src/com/mostc/pftt/runner/CliPhptTestCaseRunner.java +++ b/src/com/mostc/pftt/runner/CliPhptTestCaseRunner.java @@ -1,8 +1,19 @@ package com.mostc.pftt.runner; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.HashMap; + +import com.mostc.pftt.host.ExecOutput; import com.mostc.pftt.host.Host; +import com.mostc.pftt.host.LocalHost; import com.mostc.pftt.model.phpt.EPhptSection; import com.mostc.pftt.model.phpt.PhpBuild; +import com.mostc.pftt.model.phpt.PhpIni; import com.mostc.pftt.model.phpt.PhptTestCase; import com.mostc.pftt.model.phpt.PhptTestPack; import com.mostc.pftt.runner.PhptTestPackRunner.PhptThread; @@ -19,12 +30,15 @@ import com.mostc.pftt.util.StringUtil; */ public class CliPhptTestCaseRunner extends AbstractPhptTestCaseRunner2 { + protected ExecOutput output; + protected HashMap<String,String> env; - public CliPhptTestCaseRunner(PhptThread thread, PhptTestCase test_case, PhptTelemetryWriter twriter, Host host, ScenarioSet scenario_set, PhpBuild build, PhptTestPack test_pack) { - super(thread, test_case, twriter, host, scenario_set, build, test_pack); + public CliPhptTestCaseRunner(PhpIni ini, PhptThread thread, PhptTestCase test_case, PhptTelemetryWriter twriter, Host host, ScenarioSet scenario_set, PhpBuild build, PhptTestPack test_pack) { + super(ini, thread, test_case, twriter, host, scenario_set, build, test_pack); } - public void executeSkipIf() throws Exception { + @Override + protected String executeSkipIf() throws Exception { // Check if test should be skipped. if (test_skipif != null && test_case.containsSection(EPhptSection.SKIPIF)) { host.saveText(test_skipif, test_case.get(EPhptSection.SKIPIF)); @@ -40,17 +54,23 @@ public class CliPhptTestCaseRunner extends AbstractPhptTestCaseRunner2 { // execute SKIPIF (60 second timeout) output = host.exec(skip_cmd, Host.ONE_MINUTE, env, null, test_pack.getTestPack()); + + return output.output; } - } // end void executeSkipIf + return null; + } // end String executeSkipIf - public void executeTest() throws Exception { + @Override + protected String executeTest() throws Exception { // execute PHP to execute the TEST code ... allow up to 60 seconds for execution // if test is taking longer than 40 seconds to run, spin up an additional thread to compensate (so other non-slow tests can be executed) output = host.exec(shell_file, Host.ONE_MINUTE, null, stdin_post, test_case.isNon8BitCharset()?test_case.getCommonCharset():null, test_pack.getTestPack(), thread, 40); - } // end void executeTest + return output.output; + } // end String executeTest - public void executeClean() throws Exception { + @Override + protected void executeClean() throws Exception { if (test_case.containsSection(EPhptSection.CLEAN)) { host.saveText(test_clean, test_case.getTrim(EPhptSection.CLEAN), null); @@ -67,5 +87,86 @@ public class CliPhptTestCaseRunner extends AbstractPhptTestCaseRunner2 { host.delete(test_clean); } } // end void executeClean + + @Override + protected String getCrashedSAPIOutput() { + if (output.isCrashed()) + return output.isEmpty() ? + "PFTT: test printed nothing. was expected to print something. exited with non-zero code (probably crash): "+output.exit_code + : output.output; + else + return null; // no crash at all + } + + @Override + protected void createShellScript() throws IOException { + // useful: rm -rf `find ./ -name "*.sh"` + // + // create a .cmd (Windows batch script) or .sh (shell script) that will actually execute PHP + // this enables PHP to be executed like what PFTT does, but using a shell|batch script + shell_file = test_file + (host.isWindows() ? ".cmd" : ".sh" ); + if (shell_file.startsWith(test_pack.getTestPack())) { + shell_file = shell_file.substring(test_pack.getTestPack().length()); + if (shell_file.startsWith("/")||shell_file.startsWith("\\")) + shell_file = shell_file.substring(1); + shell_file = twriter.telem_dir+"/"+shell_file; + } + StringWriter sw = new StringWriter(); + PrintWriter fw = new PrintWriter(sw); + if (host.isWindows()) { + fw.println("@echo off"); // critical: or output will include these commands and fail test + } else { + fw.println("#!/bin/sh"); + } + fw.println("cd "+host.fixPath(test_dir)); + for ( String name : env.keySet()) { + String value = env.get(name); + if (value==null) + value = ""; + else + value = StringUtil.replaceAll(PAT_bs, "\\\\\"", StringUtil.replaceAll(PAT_dollar, "\\\\\\$", value)); + + if (host.isWindows()) { + if (value.contains(" ")) + // important: on Windows, don't use " " for script variables unless you have to + // the " " get passed into PHP so variables won't be read correctly + fw.println("set "+name+"=\""+value+"\""); + else + fw.println("set "+name+"="+value+""); + } else + fw.println("export "+name+"=\""+value+"\""); + } + fw.println(cmd); + fw.close(); + shell_script = sw.toString(); + FileWriter w = new FileWriter(shell_file); + fw = new PrintWriter(w); + fw.print(shell_script); + fw.flush(); + fw.close(); + w.close(); + } // end protected void createShellScript + + @Override + protected void prepareSTDIN() throws IOException { + String stdin_file = test_file + ".stdin"; + if (stdin_file.startsWith(test_pack.getTestPack())) { + stdin_file = stdin_file.substring(test_pack.getTestPack().length()); + if (stdin_file.startsWith("/")||stdin_file.startsWith("\\")) + stdin_file = stdin_file.substring(1); + stdin_file = twriter.telem_dir+"/"+stdin_file; + } + new File(stdin_file).getParentFile().mkdirs(); + if (stdin_post!=null) { + FileOutputStream fw = new FileOutputStream(stdin_file); + fw.write(stdin_post); + fw.close(); + } + } // end protected void prepareSTDIN + + @Override + protected String[] splitCmdString() { + return LocalHost.splitCmdString(cmd); + } } // end public class CliTestCaseRunner diff --git a/src/com/mostc/pftt/runner/HTTPTestCaseRunner.java b/src/com/mostc/pftt/runner/HTTPTestCaseRunner.java deleted file mode 100644 index 0ac831e..0000000 --- a/src/com/mostc/pftt/runner/HTTPTestCaseRunner.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.mostc.pftt.runner; - -import com.mostc.pftt.host.Host; -import com.mostc.pftt.model.phpt.PhpBuild; -import com.mostc.pftt.model.phpt.PhptTestCase; -import com.mostc.pftt.model.phpt.PhptTestPack; -import com.mostc.pftt.runner.PhptTestPackRunner.PhptThread; -import com.mostc.pftt.scenario.ScenarioSet; -import com.mostc.pftt.telemetry.PhptTelemetryWriter; - -/** Runs PHPT Test Cases against PHP while its running under a Web Server (builtin, IIS or Apache) - * - * @author Matt Ficken - * - */ - -public class HTTPTestCaseRunner extends AbstractPhptTestCaseRunner2 { - - public HTTPTestCaseRunner(PhptThread thread, PhptTestCase test_case, PhptTelemetryWriter twriter, Host host, ScenarioSet scenario_set, PhpBuild build, PhptTestPack test_pack) { - super(thread, test_case, twriter, host, scenario_set, build, test_pack); - // TODO Auto-generated constructor stub - } - - @Override - public void executeSkipIf() throws Exception { - // TODO Auto-generated method stub - - } - - @Override - public void executeTest() throws Exception { - // TODO Auto-generated method stub - - } - - @Override - public void executeClean() throws Exception { - // TODO Auto-generated method stub - - } - -} diff --git a/src/com/mostc/pftt/runner/PhptTestPackRunner.java b/src/com/mostc/pftt/runner/PhptTestPackRunner.java index 20fe5df..a0cdb61 100644 --- a/src/com/mostc/pftt/runner/PhptTestPackRunner.java +++ b/src/com/mostc/pftt/runner/PhptTestPackRunner.java @@ -2,7 +2,7 @@ package com.mostc.pftt.runner; import java.util.HashMap; import java.util.Iterator; -import java.util.LinkedList; +import java.util.List; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -11,6 +11,8 @@ import com.mostc.pftt.host.Host; import com.mostc.pftt.model.phpt.PhpBuild; import com.mostc.pftt.model.phpt.PhptTestCase; import com.mostc.pftt.model.phpt.PhptTestPack; +import com.mostc.pftt.model.sapi.SAPIInstance; +import com.mostc.pftt.model.sapi.TestCaseGroupKey; import com.mostc.pftt.scenario.AbstractSAPIScenario; import com.mostc.pftt.scenario.ScenarioSet; import com.mostc.pftt.telemetry.PhptTelemetryWriter; @@ -20,6 +22,7 @@ import com.mostc.pftt.util.HostEnvUtil; * * Can either run all PHPTs from the test pack, or PHPTs matching a set of names or name fragments. * + * TODO add @see for SAPIInstanceOrIni and concurrent or non-concurrent|serial SAPIInstanceOrInis * @author Matt Ficken * */ @@ -31,9 +34,10 @@ public class PhptTestPackRunner extends AbstractTestPackRunner { protected final PhptTelemetryWriter twriter; protected ETestPackRunnerState runner_state; protected AtomicInteger test_count, active_thread_count; - protected LinkedBlockingQueue<PhptTestCase> thread_safe_jobs; - protected HashMap<String,LinkedBlockingQueue<PhptTestCase>> jobs_by_extension; + protected HashMap<TestCaseGroupKey,LinkedBlockingQueue<PhptTestCase>> thread_safe_tests; + protected HashMap<String,HashMap<TestCaseGroupKey,LinkedBlockingQueue<PhptTestCase>>> non_thread_safe_tests; protected AbstractSAPIScenario sapi_scenario; + protected LinkedBlockingQueue<TestCaseGroupKey> group_keys; public PhptTestPackRunner(PhptTelemetryWriter twriter, PhptTestPack test_pack, ScenarioSet scenario_set, PhpBuild build, Host host) { super(scenario_set, build, host); @@ -41,71 +45,140 @@ public class PhptTestPackRunner extends AbstractTestPackRunner { this.test_pack = test_pack; } - public void run_test_list(LinkedList<PhptTestCase> test_cases) throws Exception { + public void runTestList(List<PhptTestCase> test_cases) throws Exception { runner_state = ETestPackRunnerState.RUNNING; sapi_scenario = ScenarioSet.getSAPIScenario(scenario_set); HostEnvUtil.prepareHostEnv(host); -// Scenario[] scenarios = new Scenario[]{new Scenario()}; -// -// for (Scenario scenario : scenarios ) { - //rt.scenario = scenario; + System.out.println("PFTT: loaded tests: "+test_cases.size()); - System.out.println("PFTT: loaded tests: "+test_cases.size()); - - thread_safe_jobs = new LinkedBlockingQueue<PhptTestCase>(); - jobs_by_extension = new HashMap<String,LinkedBlockingQueue<PhptTestCase>>(); + try { + groupTestCases(test_cases); + + // TODO serialSAPIInstance_executeTestCases(); + parallelSAPIInstance_executeTestCases(); + + } finally { + // be sure all running WebServerInstances, or other SAPIInstances are + // closed by end of testing (otherwise php.exe -S will keep on running) + close(); + } + } + + public void close() { + for ( TestCaseGroupKey group_key : group_keys ) { + if (group_key instanceof SAPIInstance) + ((SAPIInstance)group_key).close(); + } + } + + protected void groupTestCases(List<PhptTestCase> test_cases) throws InterruptedException { + thread_safe_tests = new HashMap<TestCaseGroupKey,LinkedBlockingQueue<PhptTestCase>>(); + non_thread_safe_tests = new HashMap<String,HashMap<TestCaseGroupKey,LinkedBlockingQueue<PhptTestCase>>>(); + + // enqueue tests + // + group_keys = new LinkedBlockingQueue<TestCaseGroupKey>(); + + boolean is_thread_safe = false; + TestCaseGroupKey group_key; + HashMap<TestCaseGroupKey,LinkedBlockingQueue<PhptTestCase>> j; + LinkedBlockingQueue<PhptTestCase> k; + for (PhptTestCase test_case : test_cases) { + is_thread_safe = true; - // enqueue tests // - // group non-thread safe tests together by PHP extension - // (extensions are tested in parallel) - for (String ext:PhptTestCase.NON_THREAD_SAFE_EXTENSIONS) { - LinkedBlockingQueue<PhptTestCase> j = new LinkedBlockingQueue<PhptTestCase>(); - jobs_by_extension.put(ext, j); - - Iterator<PhptTestCase> it = test_cases.listIterator(); - PhptTestCase f; - while (it.hasNext()) { - f = it.next(); - if (f.getName().toLowerCase().contains(ext)) { - j.put(f); - it.remove(); - } - } - } - { - // put all remaining files - Iterator<PhptTestCase> it = test_cases.listIterator(); - while (it.hasNext()) { - thread_safe_jobs.put(it.next()); + try { + if (sapi_scenario.willSkip(twriter, host, build, test_case)) { + // #willSkip will record the PhptTestResult explaining why it was skipped + continue; } + } catch ( Exception ex ) { + ex.printStackTrace(); } // - // execute tests - long start_time = System.currentTimeMillis(); - - // 80 -> 1000 + // + // TODO for cli, don't create anything just a static key + // TODO what about web servers that only allow 1 running instance ??? + group_key = sapi_scenario.createTestGroupKey(host, build, test_pack, test_case); + if (!group_keys.contains(group_key)) + group_keys.put(group_key); + // - int thread_count = INIT_THREAD_COUNT; - test_count = new AtomicInteger(0); - active_thread_count = new AtomicInteger(thread_count); + // + /* TODO for (String ext:PhptTestCase.NON_THREAD_SAFE_EXTENSIONS) { + if (test_case.getName().toLowerCase().contains(ext)) { + j = non_thread_safe_tests.get(ext); + if (j==null) + non_thread_safe_tests.put(ext, j = new HashMap<TestCaseGroupKey,LinkedBlockingQueue<PhptTestCase>>()); + k = j.get(group_key); + if (k==null) + j.put(group_key, k = new LinkedBlockingQueue<PhptTestCase>()); + k.put(test_case); + + is_thread_safe = false; + + break; + } + }*/ + // - for ( int i=0 ; i < thread_count ; i++ ) - start_thread(); + // + if (is_thread_safe) { + k = thread_safe_tests.get(group_key); + if (k==null) + thread_safe_tests.put(group_key, k = new LinkedBlockingQueue<PhptTestCase>()); + k.put(test_case); + } + // + } // end while + } // end protected void groupTestCases + + protected void parallelSAPIInstance_executeTestCases() throws InterruptedException { + int thread_count = INIT_THREAD_COUNT; + test_count = new AtomicInteger(0); + active_thread_count = new AtomicInteger(thread_count); + + thread_count = INIT_THREAD_COUNT; + for ( int i=0 ; i < thread_count ; i++ ) { + start_thread(); + } + + // wait until done + int c ; while ( ( c = active_thread_count.get() ) > 0 ) { Thread.sleep(c>3?1000:50); } + } // end protected void parallelSAPIInstance_executeTestCases + + /*protected void serialSAPIInstance_executeTestCases() throws InterruptedException { + // execute tests + long start_time = System.currentTimeMillis(); + + int thread_count = INIT_THREAD_COUNT; + test_count = new AtomicInteger(0); + active_thread_count = new AtomicInteger(thread_count); + + // + Iterator<TestCaseGroupKey> ini_it = group_keys.iterator(); + TestCaseGroupKey ini; + while (ini_it.hasNext()) { + ini = ini_it.next(); + thread_count = INIT_THREAD_COUNT; + for ( int i=0 ; i < thread_count ; i++ ) { + start_thread(ini); + } // wait until done int c ; while ( ( c = active_thread_count.get() ) > 0 ) { Thread.sleep(c>3?1000:50); } - - long run_time = Math.abs(System.currentTimeMillis() - start_time); - - //System.out.println(test_count); - System.out.println((run_time/1000)+" seconds"); - - //} // end for - } // end void + } + // + + long run_time = Math.abs(System.currentTimeMillis() - start_time); + + //System.out.println(test_count); + System.out.println((run_time/1000)+" seconds"); + } // end void serialSAPIInstance_executeTestCases + */ protected void start_thread() { PhptThread t = new PhptThread(); @@ -115,48 +188,96 @@ public class PhptTestPackRunner extends AbstractTestPackRunner { } public class PhptThread extends SlowReplacementTestPackRunnerThread { - protected AtomicBoolean run_thread = new AtomicBoolean(true); - + protected final AtomicBoolean run_thread = new AtomicBoolean(true); + @Override public void run() { - // pick a non-thread-safe(NTS) extension that isn't already running - // then run it + // pick a non-thread-safe(NTS) extension that isn't already running then run it // // keep doing that until they're all done, then execute all the thread-safe extensions - // (if there aren't enough NTS extensions to fill all the threads, some threads will execute thread-safe tests + // (if there aren't enough NTS extensions to fill all the threads, some threads will only execute thread-safe tests // - + try { + runNonThreadSafe(); + + // execute any remaining thread safe jobs + runThreadSafe(); + + } finally { + if (run_thread.get()) + // if #stopThisThread not called + active_thread_count.decrementAndGet(); + } + } // end public void run + + protected void runThreadSafe() { + TestCaseGroupKey group_key; + Iterator<TestCaseGroupKey> group_it; + LinkedBlockingQueue<PhptTestCase> jobs; while (run_thread.get()) { - LinkedBlockingQueue<PhptTestCase> jobs; - synchronized (jobs_by_extension) { - if (jobs_by_extension.isEmpty()) + synchronized (thread_safe_tests) { + group_it = thread_safe_tests.keySet().iterator(); + if (!group_it.hasNext()) + break; + + group_key = group_it.next(); + jobs = thread_safe_tests.get(group_key); + if (jobs.isEmpty()) + group_it.remove(); + } + if (group_key!=null) + exec_jobs(group_key, jobs, test_count); + } + } // end protected void runThreadSafe + + protected void runNonThreadSafe() { + TestCaseGroupKey group_key; + Iterator<String> ext_it; + Iterator<TestCaseGroupKey> group_it; + HashMap<TestCaseGroupKey,LinkedBlockingQueue<PhptTestCase>> non_thread_safe_tests_group; + LinkedBlockingQueue<PhptTestCase> jobs; + String ext_name; + while (run_thread.get()) { + synchronized (non_thread_safe_tests) { + if (non_thread_safe_tests.isEmpty()) // no more NTS extensions break; // pick a remaining NTS extension - Iterator<String> it = jobs_by_extension.keySet().iterator(); - jobs = jobs_by_extension.get(it.next()); - it.remove(); - } - exec_jobs(jobs, test_count); + ext_it = non_thread_safe_tests.keySet().iterator(); + ext_name = ext_it.next(); + + non_thread_safe_tests_group = non_thread_safe_tests.get(ext_name); + group_it = non_thread_safe_tests_group.keySet().iterator(); + group_key = null; + jobs = null; + while (group_it.hasNext()) { + group_key = group_it.next(); + jobs = non_thread_safe_tests_group.get(group_key); + if (!jobs.isEmpty()) { + group_it.remove(); + break; + } + } + } // end sync + if (group_key!=null) + exec_jobs(group_key, jobs, test_count); } - - // execute any remaining thread safe jobs - exec_jobs(thread_safe_jobs, test_count); - - if (run_thread.get()) - // if #stopThisThread not called - active_thread_count.decrementAndGet(); - } // end public void run + } // end protected void runNonThreadSafe - protected void exec_jobs(LinkedBlockingQueue<PhptTestCase> jobs, AtomicInteger test_count) { - PhptTestCase test_file; - while ( ( test_file = jobs.poll() ) != null && run_thread.get() && runner_state==ETestPackRunnerState.RUNNING) { + protected void exec_jobs(TestCaseGroupKey ini, LinkedBlockingQueue<PhptTestCase> jobs, AtomicInteger test_count) { + PhptTestCase test_case; + while ( ( + test_case = jobs.poll() + ) != null && + run_thread.get() && + runner_state==ETestPackRunnerState.RUNNING + ) { // CRITICAL: catch exception so thread will always end normally try { - sapi_scenario.createPhptTestCaseRunner(this, test_file, twriter, host, scenario_set, build, test_pack).runTest(); + sapi_scenario.createPhptTestCaseRunner(this, ini, test_case, twriter, host, scenario_set, build, test_pack).runTest(); } catch ( Throwable ex ) { - //twriter.show_exception(test_file, ex); + twriter.show_exception(test_case, ex); } test_count.incrementAndGet(); diff --git a/src/com/mostc/pftt/runner/PhptTestPackRunner.java.bak b/src/com/mostc/pftt/runner/PhptTestPackRunner.java.bak new file mode 100644 index 0000000..cf6b195 --- /dev/null +++ b/src/com/mostc/pftt/runner/PhptTestPackRunner.java.bak @@ -0,0 +1,210 @@ +package com.mostc.pftt.runner; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import com.mostc.pftt.host.Host; +import com.mostc.pftt.model.phpt.PhpBuild; +import com.mostc.pftt.model.phpt.PhptTestCase; +import com.mostc.pftt.model.phpt.PhptTestPack; +import com.mostc.pftt.scenario.AbstractSAPIScenario; +import com.mostc.pftt.scenario.ScenarioSet; +import com.mostc.pftt.telemetry.PhptTelemetryWriter; +import com.mostc.pftt.util.HostEnvUtil; + +/** Runs PHPTs from a given PhptTestPack. + * + * Can either run all PHPTs from the test pack, or PHPTs matching a set of names or name fragments. + * + * @author Matt Ficken + * + */ + +// TODO +//1. group test-cases by ini +//2. start new web server for each ini +// -use 1 WebserverSAPIMonitor instance to allocate ports +// -WebServerSAPIInstance instance for each web server process +//3a. if allowConcurrentWebserverSAPIInstances +//.1. group by SAPIInstance, creating a SAPIInstance for each INI +//.2. thread by all test-cases, tracking how many test-cases are left for a SAPI Instance and closing it when it reaches 0 +//3b. if !allowConcurrentWebserverSAPIInstances +//.1. thread by test-case until all test cases for INI/SAPIInstance executed, then close current and proceed to next SAPIInstance +// -create SAPIInstance for INI if DNE + +public class PhptTestPackRunner extends AbstractTestPackRunner { + protected static final int INIT_THREAD_COUNT = 8; // TODO 16; + protected static final int MAX_THREAD_COUNT = 32; + protected final PhptTestPack test_pack; + protected final PhptTelemetryWriter twriter; + protected ETestPackRunnerState runner_state; + protected AtomicInteger test_count, active_thread_count; + protected LinkedBlockingQueue<PhptTestCase> thread_safe_jobs; + protected HashMap<String,LinkedBlockingQueue<PhptTestCase>> jobs_by_extension; + protected AbstractSAPIScenario sapi_scenario; + + public PhptTestPackRunner(PhptTelemetryWriter twriter, PhptTestPack test_pack, ScenarioSet scenario_set, PhpBuild build, Host host) { + super(scenario_set, build, host); + this.twriter = twriter; + this.test_pack = test_pack; + } + + public void run_test_list(LinkedList<PhptTestCase> test_cases) throws Exception { + runner_state = ETestPackRunnerState.RUNNING; + sapi_scenario = ScenarioSet.getSAPIScenario(scenario_set); + + HostEnvUtil.prepareHostEnv(host); + +// Scenario[] scenarios = new Scenario[]{new Scenario()}; +// +// for (Scenario scenario : scenarios ) { + //rt.scenario = scenario; + + System.out.println("PFTT: loaded tests: "+test_cases.size()); + + thread_safe_jobs = new LinkedBlockingQueue<PhptTestCase>(); + jobs_by_extension = new HashMap<String,LinkedBlockingQueue<PhptTestCase>>(); + + // enqueue tests + // + // group non-thread safe tests together by PHP extension + // (extensions are tested in parallel) + for (String ext:PhptTestCase.NON_THREAD_SAFE_EXTENSIONS) { + LinkedBlockingQueue<PhptTestCase> j = new LinkedBlockingQueue<PhptTestCase>(); + jobs_by_extension.put(ext, j); + + Iterator<PhptTestCase> it = test_cases.listIterator(); + PhptTestCase f; + while (it.hasNext()) { + f = it.next(); + if (f.getName().toLowerCase().contains(ext)) { + j.put(f); + it.remove(); + } + } + } + { + // put all remaining files + Iterator<PhptTestCase> it = test_cases.listIterator(); + while (it.hasNext()) { + thread_safe_jobs.put(it.next()); + } + } + // + + // execute tests + long start_time = System.currentTimeMillis(); + + // 80 -> 1000 + + int thread_count = INIT_THREAD_COUNT; + test_count = new AtomicInteger(0); + active_thread_count = new AtomicInteger(thread_count); + + for ( int i=0 ; i < thread_count ; i++ ) + start_thread(); + + // wait until done + int c ; while ( ( c = active_thread_count.get() ) > 0 ) { Thread.sleep(c>3?1000:50); } + + long run_time = Math.abs(System.currentTimeMillis() - start_time); + + //System.out.println(test_count); + System.out.println((run_time/1000)+" seconds"); + + //} // end for + } // end void + + protected void start_thread() { + PhptThread t = new PhptThread(); + // if running Swing UI, run thread minimum priority in favor of Swing EDT + t.setPriority(Thread.MIN_PRIORITY); + t.start(); + } + + public class PhptThread extends SlowReplacementTestPackRunnerThread { + protected AtomicBoolean run_thread = new AtomicBoolean(true); + + @Override + public void run() { + // pick a non-thread-safe(NTS) extension that isn't already running + // then run it + // + // keep doing that until they're all done, then execute all the thread-safe extensions + // (if there aren't enough NTS extensions to fill all the threads, some threads will execute thread-safe tests + // + + while (run_thread.get()) { + LinkedBlockingQueue<PhptTestCase> jobs; + synchronized (jobs_by_extension) { + if (jobs_by_extension.isEmpty()) + // no more NTS extensions + break; + + // pick a remaining NTS extension + Iterator<String> it = jobs_by_extension.keySet().iterator(); + jobs = jobs_by_extension.get(it.next()); + it.remove(); + } + exec_jobs(jobs, test_count); + } + + // execute any remaining thread safe jobs + exec_jobs(thread_safe_jobs, test_count); + + if (run_thread.get()) + // if #stopThisThread not called + active_thread_count.decrementAndGet(); + } // end public void run + + protected void exec_jobs(LinkedBlockingQueue<PhptTestCase> jobs, AtomicInteger test_count) { + PhptTestCase test_file; + while ( ( test_file = jobs.poll() ) != null && run_thread.get() && runner_state==ETestPackRunnerState.RUNNING) { + // CRITICAL: catch exception so thread will always end normally + try { + sapi_scenario.createPhptTestCaseRunner(this, test_file, twriter, host, scenario_set, build, test_pack).runTest(); + } catch ( Throwable ex ) { + //twriter.show_exception(test_file, ex); + } + + test_count.incrementAndGet(); + + //Thread.yield() + } + } // end protected void exec_jobs + + @Override + protected boolean slowCreateNewThread() { + return false; // TODO active_thread_count.get() < MAX_THREAD_COUNT; + } + + @Override + protected void createNewThread() { + start_thread(); + } + + @Override + protected void stopThisThread() { + // continue running current CliTestCaseRunner, but don't start any more of them + run_thread.set(false); + + active_thread_count.decrementAndGet(); + } + + } // end public class PhptThread + + @Override + public void setState(ETestPackRunnerState state) throws IllegalStateException { + this.runner_state = state; + } + + @Override + public ETestPackRunnerState getState() { + return runner_state; + } + +} // end class TestPackRunner diff --git a/src/com/mostc/pftt/scenario/AbstractApacheScenario.java b/src/com/mostc/pftt/scenario/AbstractApacheScenario.java index 0d6725f..612d6e1 100644 --- a/src/com/mostc/pftt/scenario/AbstractApacheScenario.java +++ b/src/com/mostc/pftt/scenario/AbstractApacheScenario.java @@ -1,5 +1,7 @@ package com.mostc.pftt.scenario; +import com.mostc.pftt.model.sapi.ApacheManager; + /** Scenarios for testing managing and testing Apache * * @author Matt Ficken @@ -8,4 +10,8 @@ package com.mostc.pftt.scenario; public abstract class AbstractApacheScenario extends AbstractWebServerScenario { + public AbstractApacheScenario() { + super(new ApacheManager()); + } + } diff --git a/src/com/mostc/pftt/scenario/AbstractIISScenario.java b/src/com/mostc/pftt/scenario/AbstractIISScenario.java index ed1dd36..d653c9c 100644 --- a/src/com/mostc/pftt/scenario/AbstractIISScenario.java +++ b/src/com/mostc/pftt/scenario/AbstractIISScenario.java @@ -1,5 +1,7 @@ package com.mostc.pftt.scenario; +import com.mostc.pftt.model.sapi.IISManager; + /** Abstract scenario for managing and testing IIS * * @author Matt Ficken @@ -7,5 +9,15 @@ package com.mostc.pftt.scenario; */ public abstract class AbstractIISScenario extends AbstractWebServerScenario { + + public AbstractIISScenario() { + super(new IISManager()); + } + public abstract boolean isExpress(); + + public boolean isStandard() { + return !isExpress(); + } + } diff --git a/src/com/mostc/pftt/scenario/AbstractSAPIScenario.java b/src/com/mostc/pftt/scenario/AbstractSAPIScenario.java index 273322c..8475fcc 100644 --- a/src/com/mostc/pftt/scenario/AbstractSAPIScenario.java +++ b/src/com/mostc/pftt/scenario/AbstractSAPIScenario.java @@ -4,8 +4,8 @@ import com.mostc.pftt.host.Host; import com.mostc.pftt.model.phpt.PhpBuild; import com.mostc.pftt.model.phpt.PhptTestCase; import com.mostc.pftt.model.phpt.PhptTestPack; +import com.mostc.pftt.model.sapi.TestCaseGroupKey; import com.mostc.pftt.runner.AbstractPhptTestCaseRunner; -import com.mostc.pftt.runner.CliPhptTestCaseRunner; import com.mostc.pftt.runner.PhptTestPackRunner.PhptThread; import com.mostc.pftt.telemetry.PhptTelemetryWriter; @@ -26,6 +26,7 @@ public abstract class AbstractSAPIScenario extends AbstractSerialScenario { /** creates a runner to run a single PhptTestCase under this SAPI scenario * * @param thread + * @param ini * @param test_case * @param twriter * @param host @@ -34,8 +35,13 @@ public abstract class AbstractSAPIScenario extends AbstractSerialScenario { * @param test_pack * @return */ - public AbstractPhptTestCaseRunner createPhptTestCaseRunner(PhptThread thread, PhptTestCase test_case, PhptTelemetryWriter twriter, Host host, ScenarioSet scenario_set, PhpBuild build, PhptTestPack test_pack) { - return new CliPhptTestCaseRunner(thread, test_case, twriter, host, scenario_set, build, test_pack); + // TODO rename ini + public abstract AbstractPhptTestCaseRunner createPhptTestCaseRunner(PhptThread thread, TestCaseGroupKey ini, PhptTestCase test_case, PhptTelemetryWriter twriter, Host host, ScenarioSet scenario_set, PhpBuild build, PhptTestPack test_pack); + + public abstract TestCaseGroupKey createTestGroupKey(Host host, PhpBuild build, PhptTestPack test_pack, PhptTestCase test_case); + + public boolean willSkip(PhptTelemetryWriter twriter, Host host, PhpBuild build, PhptTestCase test_case) throws Exception { + return AbstractPhptTestCaseRunner.willSkip(twriter, host, build, test_case); } } diff --git a/src/com/mostc/pftt/scenario/AbstractWebServerScenario.java b/src/com/mostc/pftt/scenario/AbstractWebServerScenario.java index 108c2a7..4f3fa99 100644 --- a/src/com/mostc/pftt/scenario/AbstractWebServerScenario.java +++ b/src/com/mostc/pftt/scenario/AbstractWebServerScenario.java @@ -4,22 +4,44 @@ import com.mostc.pftt.host.Host; import com.mostc.pftt.model.phpt.PhpBuild; import com.mostc.pftt.model.phpt.PhptTestCase; import com.mostc.pftt.model.phpt.PhptTestPack; +import com.mostc.pftt.model.sapi.TestCaseGroupKey; +import com.mostc.pftt.model.sapi.WebServerInstance; +import com.mostc.pftt.model.sapi.WebServerManager; import com.mostc.pftt.runner.AbstractPhptTestCaseRunner; -import com.mostc.pftt.runner.HTTPTestCaseRunner; +import com.mostc.pftt.runner.HttpTestCaseRunner; import com.mostc.pftt.runner.PhptTestPackRunner.PhptThread; import com.mostc.pftt.telemetry.PhptTelemetryWriter; -/** +/** scenarios for testing PHP while its running under a web server + * * @see SSLSocketScenario * @author Matt Ficken * */ public abstract class AbstractWebServerScenario extends AbstractSAPIScenario { - + protected final WebServerManager smgr; + + protected AbstractWebServerScenario(WebServerManager smgr) { + this.smgr = smgr; + } + @Override - public AbstractPhptTestCaseRunner createPhptTestCaseRunner(PhptThread thread, PhptTestCase test_case, PhptTelemetryWriter twriter, Host host, ScenarioSet scenario_set, PhpBuild build, PhptTestPack test_pack) { - return new HTTPTestCaseRunner(thread, test_case, twriter, host, scenario_set, build, test_pack); + public AbstractPhptTestCaseRunner createPhptTestCaseRunner(PhptThread thread, TestCaseGroupKey ini, PhptTestCase test_case, PhptTelemetryWriter twriter, Host host, ScenarioSet scenario_set, PhpBuild build, PhptTestPack test_pack) { + return new HttpTestCaseRunner(smgr, (WebServerInstance)ini, thread, test_case, twriter, host, scenario_set, build, test_pack); + } + + WebServerInstance web; // TODO temp + @Override + public synchronized TestCaseGroupKey createTestGroupKey(Host host, PhpBuild build, PhptTestPack test_pack, PhptTestCase test_case) { + // TODO use HttpTestCaseRunner to get docroot from test_pack + if (web!=null) + return web; + return web = smgr.getWebServerInstance(host, build, AbstractPhptTestCaseRunner.createIniForTest(host, build, test_pack, test_case), test_pack.getTestPack(), null); + } + + public boolean willSkip(PhptTelemetryWriter twriter, Host host, PhpBuild build, PhptTestCase test_case) throws Exception { + return HttpTestCaseRunner.willSkip(twriter, host, build, test_case); } -} +} // end public abstract class AbstractWebServerScenario diff --git a/src/com/mostc/pftt/scenario/BuiltinWWWScenario.java b/src/com/mostc/pftt/scenario/BuiltinWWWScenario.java deleted file mode 100644 index f07a719..0000000 --- a/src/com/mostc/pftt/scenario/BuiltinWWWScenario.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.mostc.pftt.scenario; - -/** Tests PHP using PHP 5.4's builtin web server. (NOT IMPLEMENTED) - * - * This is the web server that's run when a user runs: php -S - * - * @author Matt Ficken - * - */ - -public class BuiltinWWWScenario extends AbstractWebServerScenario { - - @Override - public String getName() { - return "Builtin-WWW"; - } - - @Override - public boolean isImplemented() { - return false; - } - -} diff --git a/src/com/mostc/pftt/scenario/BuiltinWebServerScenario.java b/src/com/mostc/pftt/scenario/BuiltinWebServerScenario.java new file mode 100644 index 0000000..55b9466 --- /dev/null +++ b/src/com/mostc/pftt/scenario/BuiltinWebServerScenario.java @@ -0,0 +1,47 @@ +package com.mostc.pftt.scenario; + +import com.mostc.pftt.host.Host; +import com.mostc.pftt.model.phpt.EBuildBranch; +import com.mostc.pftt.model.phpt.PhpBuild; +import com.mostc.pftt.model.sapi.BuiltinWebServerManager; + +/** Tests PHP using PHP's builtin web server. + * + * This is the web server that's run when a user runs: php -S + * + * This feature is only available (this scenario can only be run against) PHP 5.4+ (not PHP 5.3) + * + * @author Matt Ficken + * + */ + +public class BuiltinWebServerScenario extends AbstractWebServerScenario { + + protected BuiltinWebServerScenario() { + super(new BuiltinWebServerManager()); + } + + /** don't run this scenario on PHP 5.3 + * + */ + @Override + public boolean isSupported(Host host, PhpBuild build) { + try { + return build.getVersionBranch(host) != EBuildBranch.PHP_5_3; + } catch (Exception e) { + e.printStackTrace(); + return true; + } + } + + @Override + public String getName() { + return "Builtin-Web-Server"; + } + + @Override + public boolean isImplemented() { + return true; + } + +} diff --git a/src/com/mostc/pftt/scenario/CLIScenario.java b/src/com/mostc/pftt/scenario/CLIScenario.java index 7bd6594..7844719 100644 --- a/src/com/mostc/pftt/scenario/CLIScenario.java +++ b/src/com/mostc/pftt/scenario/CLIScenario.java @@ -1,5 +1,16 @@ package com.mostc.pftt.scenario; +import com.mostc.pftt.host.Host; +import com.mostc.pftt.model.phpt.PhpBuild; +import com.mostc.pftt.model.phpt.PhpIni; +import com.mostc.pftt.model.phpt.PhptTestCase; +import com.mostc.pftt.model.phpt.PhptTestPack; +import com.mostc.pftt.model.sapi.TestCaseGroupKey; +import com.mostc.pftt.runner.AbstractPhptTestCaseRunner; +import com.mostc.pftt.runner.CliPhptTestCaseRunner; +import com.mostc.pftt.runner.PhptTestPackRunner.PhptThread; +import com.mostc.pftt.telemetry.PhptTelemetryWriter; + /** Tests the Command Line Interface(CLI) for running PHP. * * @author Matt Ficken @@ -18,4 +29,17 @@ public class CLIScenario extends AbstractSAPIScenario { return true; } + @Override + public AbstractPhptTestCaseRunner createPhptTestCaseRunner( + PhptThread thread, TestCaseGroupKey ini, PhptTestCase test_case, + PhptTelemetryWriter twriter, Host host, ScenarioSet scenario_set, + PhpBuild build, PhptTestPack test_pack) { + return new CliPhptTestCaseRunner((PhpIni)ini, thread, test_case, twriter, host, scenario_set, build, test_pack); + } + + @Override + public TestCaseGroupKey createTestGroupKey(Host host, PhpBuild build, PhptTestPack test_pack, PhptTestCase test_case) { + return AbstractPhptTestCaseRunner.createIniForTest(host, build, test_pack, test_case); + } + } diff --git a/src/com/mostc/pftt/scenario/IISExpressFastCGIScenario.java b/src/com/mostc/pftt/scenario/IISExpressFastCGIScenario.java index c263847..facadab 100644 --- a/src/com/mostc/pftt/scenario/IISExpressFastCGIScenario.java +++ b/src/com/mostc/pftt/scenario/IISExpressFastCGIScenario.java @@ -18,4 +18,9 @@ public class IISExpressFastCGIScenario extends AbstractIISFastCGIScenario { return false; } + @Override + public boolean isExpress() { + return true; + } + } diff --git a/src/com/mostc/pftt/scenario/IISStandardFastCGIScenario.java b/src/com/mostc/pftt/scenario/IISStandardFastCGIScenario.java index ad69ca0..4c7f817 100644 --- a/src/com/mostc/pftt/scenario/IISStandardFastCGIScenario.java +++ b/src/com/mostc/pftt/scenario/IISStandardFastCGIScenario.java @@ -18,4 +18,9 @@ public class IISStandardFastCGIScenario extends AbstractIISFastCGIScenario { return false; } + @Override + public boolean isExpress() { + return false; + } + } diff --git a/src/com/mostc/pftt/scenario/Scenario.java b/src/com/mostc/pftt/scenario/Scenario.java index cfa18bc..f23bb59 100644 --- a/src/com/mostc/pftt/scenario/Scenario.java +++ b/src/com/mostc/pftt/scenario/Scenario.java @@ -1,5 +1,8 @@ package com.mostc.pftt.scenario; +import com.mostc.pftt.host.Host; +import com.mostc.pftt.model.phpt.PhpBuild; + /** Scenario to test PHP under. * * Often a whole set of Scenarios (@see ScenarioSet) are used together. @@ -24,6 +27,10 @@ public abstract class Scenario { public abstract String getName(); public abstract boolean isImplemented(); + public boolean isSupported(Host host, PhpBuild build) { + return true; + } + @Override public String toString() { return getName(); @@ -48,8 +55,8 @@ public abstract class Scenario { }, // SAPIs new Scenario[]{ - new CLIScenario(), - new BuiltinWWWScenario(), + // TODO new CLIScenario(), + new BuiltinWebServerScenario(), new ApacheModPHPScenario(), new IISExpressFastCGIScenario(), new IISStandardFastCGIScenario() @@ -83,4 +90,4 @@ public abstract class Scenario { }; } -} +} // end public abstract class Scenario diff --git a/src/com/mostc/pftt/scenario/ScenarioSet.java b/src/com/mostc/pftt/scenario/ScenarioSet.java index 0268dc3..2d73295 100644 --- a/src/com/mostc/pftt/scenario/ScenarioSet.java +++ b/src/com/mostc/pftt/scenario/ScenarioSet.java @@ -5,6 +5,7 @@ import java.util.Arrays; import java.util.List; import com.mostc.pftt.host.Host; +import com.mostc.pftt.model.phpt.PhpBuild; /** A Set of Scenarios to test PHP under. * @@ -58,11 +59,15 @@ public class ScenarioSet extends ArrayList<Scenario> { /** determines if this set of scenarios can be executed on the given host * * @param host + * @param build * @return */ - public boolean isSupported(Host host) { - // TODO - return false; + public boolean isSupported(Host host, PhpBuild build) { + for (Scenario s :this) { + if (!s.isSupported(host, build)) + return false; + } + return true; } @Override diff --git a/src/com/mostc/pftt/telemetry/PhptTelemetryReader.java b/src/com/mostc/pftt/telemetry/PhptTelemetryReader.java index 71e8e1e..b044a15 100644 --- a/src/com/mostc/pftt/telemetry/PhptTelemetryReader.java +++ b/src/com/mostc/pftt/telemetry/PhptTelemetryReader.java @@ -98,7 +98,7 @@ public class PhptTelemetryReader extends PhptTelemetry { return tally.unsupported; case BORK: return tally.bork; - case EXCEPTION: + case INTERNAL_EXCEPTION: return tally.exception; } return 0; diff --git a/src/com/mostc/pftt/telemetry/PhptTelemetryWriter.java b/src/com/mostc/pftt/telemetry/PhptTelemetryWriter.java index edcf96d..1eeb2c0 100644 --- a/src/com/mostc/pftt/telemetry/PhptTelemetryWriter.java +++ b/src/com/mostc/pftt/telemetry/PhptTelemetryWriter.java @@ -93,7 +93,7 @@ public class PhptTelemetryWriter extends PhptTelemetry { tally.xfail_works = counts.get(EPhptTestStatus.XFAIL_WORKS).get(); tally.unsupported = counts.get(EPhptTestStatus.UNSUPPORTED).get(); tally.bork = counts.get(EPhptTestStatus.BORK).get(); - tally.exception = counts.get(EPhptTestStatus.EXCEPTION).get(); + tally.exception = counts.get(EPhptTestStatus.INTERNAL_EXCEPTION).get(); FileWriter fw = new FileWriter(new File(telem_dir, "tally.xml")); PhptTallyFile.write(tally, fw); fw.close(); @@ -160,7 +160,8 @@ public class PhptTelemetryWriter extends PhptTelemetry { String ex_str = ErrorUtil.toString(ex); // count exceptions as a result (the worst kind of failure, a pftt failure) - addResult(new PhptTestResult(host, EPhptTestStatus.EXCEPTION, test_case, ex_str, null, null, null, null, null, null, null, null, null, null)); + addResult(new PhptTestResult(host, EPhptTestStatus.INTERNAL_EXCEPTION, test_case, ex_str, null, null, null, null, null, null, null, null, null, null)); + // TODO show count of exceptions in gui } int completed = 0; // XXX public void addResult(PhptTestResult result) { diff --git a/src/com/mostc/pftt/telemetry/PhptTestResult.java b/src/com/mostc/pftt/telemetry/PhptTestResult.java index 23b8311..edfcee0 100644 --- a/src/com/mostc/pftt/telemetry/PhptTestResult.java +++ b/src/com/mostc/pftt/telemetry/PhptTestResult.java @@ -17,6 +17,17 @@ import com.mostc.pftt.host.Host; import com.mostc.pftt.model.phpt.EPhptTestStatus; import com.mostc.pftt.model.phpt.PhptTestCase; +/** result of running a PhptTestCase + * + * the status of PhptTestCase is determined by: + * -executing SKIPIF section(optional) + * -comparing the actual and EXPECT, EXPECTF or EXPECTREGEX sections + * -certain sections(ENV, STDIN) if present, are skipped for HTTP testing + * + * @author Matt Ficken + * + */ + public class PhptTestResult { public final Host host; /** the result of the test case */ @@ -45,9 +56,17 @@ public class PhptTestResult { /** the difference (as string) between the actual and expected output */ @Nullable public final String diff_str; + protected final String sapi_output; public PhptTestResult(Host host, EPhptTestStatus status, PhptTestCase test_case, String actual, String[] actual_lines, String[] expected_lines, Charset actual_cs, Map<String,String> env, String[] cmd_array, byte[] stdin_data, String shell_script, Diff<String> diff, String expectf_output, String preoverride_actual) { + this(host, status, test_case, actual, actual_lines, expected_lines, actual_cs, env, cmd_array, stdin_data, shell_script, diff, expectf_output, preoverride_actual, null); + } + + public PhptTestResult(Host host, EPhptTestStatus status, PhptTestCase test_case, String actual, String[] actual_lines, String[] expected_lines, Charset actual_cs, Map<String,String> env, String[] cmd_array, byte[] stdin_data, String shell_script, Diff<String> diff, String expectf_output, String preoverride_actual, String sapi_output) { actual = (""+actual_cs)+"\n"+actual; // TODO + + this.sapi_output = sapi_output; + this.host = host; this.status = status; this.test_case = test_case; @@ -64,7 +83,12 @@ public class PhptTestResult { this.diff_str = toString(actual_lines, diff, expected_lines); else this.diff_str = null; - } + } + + public String getSAPIOutput() { + // TODO should store this once for all test cases run under the same instance of a SAPI + return sapi_output; + } public void write(File telem_dir) throws IOException { if (status==EPhptTestStatus.FAIL) { diff --git a/src/com/mostc/pftt/ui/ExpectedActualDiffPHPTDisplay.java b/src/com/mostc/pftt/ui/ExpectedActualDiffPHPTDisplay.java index b2f2d9b..abe3f7f 100644 --- a/src/com/mostc/pftt/ui/ExpectedActualDiffPHPTDisplay.java +++ b/src/com/mostc/pftt/ui/ExpectedActualDiffPHPTDisplay.java @@ -36,6 +36,7 @@ import com.mostc.pftt.util.StringUtil; import se.datadosen.component.RiverLayout; +@SuppressWarnings("serial") public class ExpectedActualDiffPHPTDisplay extends JScrollPane { protected JScrollPane horizontal_jsp, vertical_jsp; protected JPanel horizontal_panel, horizontal_button_panel, vertical_panel; @@ -44,7 +45,7 @@ public class ExpectedActualDiffPHPTDisplay extends JScrollPane { protected JTable env_table; protected DefaultListModel cmd_array_list_model; protected JList cmd_array_list; - protected JTextArea stdin_data_textarea, shell_script_textarea, expectf_textarea, pre_override_textarea; + protected JTextArea stdin_data_textarea, shell_script_textarea, expectf_textarea, pre_override_textarea, sapi_output_textarea; protected PhptTestResult test; public ExpectedActualDiffPHPTDisplay() { @@ -74,6 +75,8 @@ public class ExpectedActualDiffPHPTDisplay extends JScrollPane { expectf_textarea.setToolTipText("EXPECTF section after regular expression patterns are added"); pre_override_textarea = new JTextArea(); pre_override_textarea.setToolTipText("Actual test output before any OS specific overrides applied"); + sapi_output_textarea = new JTextArea(); + sapi_output_textarea.setToolTipText("Output from SAPI - did web server crash? etc..."); horizontal_button_panel.add(actual_display.button_panel); horizontal_button_panel.add(expected_display.button_panel); @@ -89,7 +92,9 @@ public class ExpectedActualDiffPHPTDisplay extends JScrollPane { vertical_panel.add(expectf_textarea); vertical_panel.add(pre_override_textarea); - JPanel prepared_panel = new JPanel(new GridLayout(4, 1, 2, 2)); + + JPanel prepared_panel = new JPanel(new GridLayout(5, 1, 2, 2)); + vertical_panel.add(new JScrollPane(sapi_output_textarea)); prepared_panel.add(new JScrollPane(stdin_data_textarea = new JTextArea())); stdin_data_textarea.setLineWrap(true); prepared_panel.add(new JScrollPane(shell_script_textarea = new JTextArea())); @@ -141,6 +146,10 @@ public class ExpectedActualDiffPHPTDisplay extends JScrollPane { diff_display.showFile(test.test_case, test.diff_str==null?"":test.diff_str); expectf_textarea.setText(test.expectf_output==null?"":test.expectf_output); pre_override_textarea.setText(test.preoverride_actual==null?"":test.preoverride_actual); + // + String sapi_output = test.getSAPIOutput(); + sapi_output_textarea.setText(sapi_output==null?"":sapi_output); + // SwingUtilities.invokeLater(new Runnable() { public void run() { diff --git a/src/com/mostc/pftt/ui/PhptHostTab.java b/src/com/mostc/pftt/ui/PhptHostTab.java index 2e6e7c4..e112b70 100644 --- a/src/com/mostc/pftt/ui/PhptHostTab.java +++ b/src/com/mostc/pftt/ui/PhptHostTab.java @@ -32,26 +32,28 @@ import com.mostc.pftt.runner.PhptTestPackRunner; import com.mostc.pftt.runner.AbstractTestPackRunner.ETestPackRunnerState; import com.mostc.pftt.telemetry.PhptTelemetry; import com.mostc.pftt.telemetry.PhptTestResult; +import com.mostc.pftt.util.StringUtil; import se.datadosen.component.RiverLayout; +@SuppressWarnings("serial") public class PhptHostTab extends JSplitPane { - JPanel panel, button_panel; - JProgressBar progress_bar, pass_bar; - JButton stop_button, prev_file_button, prev_button, next_file_button, next_button, to_actual_button, to_expect_button, ignore_button, skip_button, pass_button; - JLabel pass_label, total_label, fail_label, xfail_label, xfail_works_label, skip_label, xskip_label, bork_label, unsupported_label, exceptions_label; - JMenuBar jmb; - JMenu options_menu, status_list_menu; - JRadioButtonMenuItem list_fail_rb; - ExpectedActualDiffPHPTDisplay eat_display; - JCheckBoxMenuItem host_console_cb; - JSplitPane jsp; - DefaultListModel fail_list_model, xfail_list_model, xfail_works_list_model, xskip_list_model, skip_list_model, pass_list_model, bork_list_model, unsupported_list_model, exceptions_list_model; - JList test_list; - JScrollPane test_list_jsp; - ConsoleTextEditor host_console; - Host host; - JRadioButtonMenuItem list_xfail_rb, list_xfail_works_rb, list_skip_rb, list_xskip_rb, list_pass_rb, list_bork_rb, list_unsupported_rb, list_exceptions_rb; + protected JPanel panel, button_panel; + protected JProgressBar progress_bar, pass_bar; + protected JButton stop_button, prev_file_button, prev_button, next_file_button, next_button, to_actual_button, to_expect_button, ignore_button, skip_button, pass_button; + protected JLabel pass_label, total_label, fail_label, crash_label, xfail_label, xfail_works_label, skip_label, xskip_label, bork_label, unsupported_label, exceptions_label; + protected JMenuBar jmb; + protected JMenu options_menu, status_list_menu; + protected JRadioButtonMenuItem list_fail_rb; + protected ExpectedActualDiffPHPTDisplay eat_display; + protected JCheckBoxMenuItem host_console_cb; + protected JSplitPane jsp; + protected DefaultListModel fail_list_model, crash_list_model, xfail_list_model, xfail_works_list_model, xskip_list_model, skip_list_model, pass_list_model, bork_list_model, unsupported_list_model, exceptions_list_model; + protected JList test_list; + protected JScrollPane test_list_jsp; + protected ConsoleTextEditor host_console; + protected Host host; + protected JRadioButtonMenuItem list_xfail_rb, list_crash_rb, list_xfail_works_rb, list_skip_rb, list_xskip_rb, list_pass_rb, list_bork_rb, list_unsupported_rb, list_exceptions_rb; public PhptHostTab(Host host, final PhptTestPackRunner phpt_test_pack_runner) { super(JSplitPane.VERTICAL_SPLIT); @@ -63,6 +65,7 @@ public class PhptHostTab extends JSplitPane { host_console.setVisible(false); fail_list_model = new DefaultListModel(); + crash_list_model = new DefaultListModel(); xfail_list_model = new DefaultListModel(); xfail_works_list_model = new DefaultListModel(); xskip_list_model = new DefaultListModel(); @@ -108,6 +111,8 @@ public class PhptHostTab extends JSplitPane { panel.add(unsupported_label = new JLabel("0")); panel.add(new JLabel("Exceptions:")); panel.add(exceptions_label = new JLabel("0")); + panel.add(new JLabel("Crashes:")); + panel.add(crash_label = new JLabel("0")); /////////////// @@ -203,12 +208,18 @@ public class PhptHostTab extends JSplitPane { showList(unsupported_list_model); } }); list_button_group.add(list_unsupported_rb); - status_list_menu.add(list_exceptions_rb = new JRadioButtonMenuItem("Exceptions")); + status_list_menu.add(list_exceptions_rb = new JRadioButtonMenuItem("Internal Exceptions")); list_exceptions_rb.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if (list_exceptions_rb.isSelected()) showList(exceptions_list_model); } }); list_button_group.add(list_exceptions_rb); + status_list_menu.add(list_crash_rb = new JRadioButtonMenuItem("Crash")); + list_crash_rb.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { + if (list_crash_rb.isSelected()) + showList(crash_list_model); + } }); + list_button_group.add(list_crash_rb); options_menu.add(host_console_cb = new JCheckBoxMenuItem("Host Console")); host_console_cb.addActionListener(new ActionListener() { @@ -279,13 +290,21 @@ public class PhptHostTab extends JSplitPane { jsp.setDividerLocation(0.75d); } - protected int pass, fail, xfail, xfail_works, skip, xskip, bork, unsupported, exceptions; // XXX duplicates functionality from PhptTelemetry + protected int crash, pass, fail, xfail, xfail_works, skip, xskip, bork, unsupported, exceptions; // XXX duplicates functionality from PhptTelemetry public void showResult(final int total, final int completed, final PhptTestResult result) { SwingUtilities.invokeLater(new Runnable() { public void run() { progress_bar.setMaximum(total); + // + if (StringUtil.isNotEmpty(result.getSAPIOutput())) { + crash++; + crash_label.setText(Integer.toString(crash)); + crash_list_model.addElement(result); + } + // + switch(result.status) { case FAIL: fail++; diff --git a/src/com/mostc/pftt/util/StringUtil.java b/src/com/mostc/pftt/util/StringUtil.java index 08dff56..559c57e 100644 --- a/src/com/mostc/pftt/util/StringUtil.java +++ b/src/com/mostc/pftt/util/StringUtil.java @@ -204,6 +204,10 @@ public final class StringUtil { return out; } + + public static int hashCode(String a) { + return a == null ? 0 : a.hashCode(); + } private StringUtil() {} diff --git a/src/com/mostc/pftt/util/WindowsSnapshotDownloadUtil.groovy b/src/com/mostc/pftt/util/WindowsSnapshotDownloadUtil.groovy index b218869..e37d940 100644 --- a/src/com/mostc/pftt/util/WindowsSnapshotDownloadUtil.groovy +++ b/src/com/mostc/pftt/util/WindowsSnapshotDownloadUtil.groovy @@ -182,6 +182,9 @@ final class WindowsSnapshotDownloadUtil { for ( def link : links ) revisions.add(link.text()); + + // critical to getting the newest revision first + Collections.reverse(revisions); ArrayList<URL> urls = new ArrayList<URL>(revisions.size()); for ( def revision : revisions )