diff options
author | 2016-06-30 15:46:10 +0000 | |
---|---|---|
committer | 2016-07-01 07:09:55 +0000 | |
commit | dc174c400c197983a984b2f28e64eeb66fb92c0a (patch) | |
tree | ea8ca3c3500453c4ae2455b16310b72617e03d77 /src/test/java/com/google/devtools/build | |
parent | 7e9358786a756fd8362b9577e94946235114b182 (diff) |
Add native process management for Windows and its Java bindings (without a sane Java API for now)
--
MOS_MIGRATED_REVID=126306559
Diffstat (limited to 'src/test/java/com/google/devtools/build')
4 files changed, 541 insertions, 0 deletions
diff --git a/src/test/java/com/google/devtools/build/lib/BUILD b/src/test/java/com/google/devtools/build/lib/BUILD index 6f890c67ad..45763d0c69 100644 --- a/src/test/java/com/google/devtools/build/lib/BUILD +++ b/src/test/java/com/google/devtools/build/lib/BUILD @@ -133,6 +133,8 @@ java_test( ], ) +# Tests that test Windows-specific functionality that run on other operating +# systems java_test( name = "windows_test", srcs = [ @@ -151,6 +153,7 @@ java_test( "//src/main/java/com/google/devtools/build/lib:inmemoryfs", "//src/main/java/com/google/devtools/build/lib:util", "//src/main/java/com/google/devtools/build/lib:vfs", + "//src/main/java/com/google/devtools/build/lib:windows", "//src/main/java/com/google/devtools/common/options", "//third_party:guava", "//third_party:guava-testlib", @@ -159,6 +162,32 @@ java_test( ], ) +# Tests that need to run on Windows +java_test( + name = "windows-tests", + srcs = glob( + ["windows/*.java"], + exclude = ["windows/MockSubprocess.java"], + ), + data = [ + ":MockSubprocess_deploy.jar", + ] + select({ + "//src:windows": ["//src/main/native:windows_jni.dll"], + "//conditions:default": [ + "//src/main/native:libunix.dylib", + "//src/main/native:libunix.so", + ], + }), + test_class = "com.google.devtools.build.lib.AllTests", + deps = [ + ":test_runner", + ":testutil", + "//src/main/java/com/google/devtools/build/lib:os_util", + "//src/main/java/com/google/devtools/build/lib:windows", + "//third_party:truth", + ], +) + java_library( name = "actions_testutil", srcs = glob([ @@ -980,6 +1009,11 @@ java_test( ], ) +java_binary( + name = "MockSubprocess", + srcs = ["windows/MockSubprocess.java"], +) + java_library( name = "ExampleWorker-lib", srcs = glob(["worker/ExampleWorker*.java"]), diff --git a/src/test/java/com/google/devtools/build/lib/windows/MockSubprocess.java b/src/test/java/com/google/devtools/build/lib/windows/MockSubprocess.java new file mode 100644 index 0000000000..7e2d90cef6 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/windows/MockSubprocess.java @@ -0,0 +1,94 @@ +// Copyright 2016 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.devtools.build.lib.windows; + +import java.io.PrintStream; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +/** + * Mock subprocess to be used for testing Windows process management. Command line usage: + * + * <ul> + * <li><code>I<register><count></code>: Read count bytes to the specified register + * <li><code>O-<string></code>: Write a string to stdout</li> + * <li><code>E-<string></code>: Write a string to stderr</li> + * <li><code>O$<variable></code>: Write an environment variable to stdout</li> + * <li><code>E$<variable></code>: Write an environment variable to stderr</li> + * <li><code>O<register></code>: Write the contents of a register to stdout</li> + * <li><code>E<register></code>: Write the contents of a register to stderr</li> + * <li><code>X<exit code%gt;</code>: Exit with the specified exit code</li> + * </ul> + * + * <p>Registers are single characters. Each command line argument is interpreted as a single + * operation. Example: + * + * <code> + * Ia10 Oa Oa Ea E-OVER X42 + * </code> + * + * Means: read 10 bytes from stdin, write them back twice to stdout and once to stderr, write + * the string "OVER" to stderr then exit with exit code 42. + */ +public class MockSubprocess { + private static Map<Character, byte[]> registers = new HashMap<>(); + + private static void writeBytes(PrintStream stream, String arg) throws Exception { + byte[] buf; + switch (arg.charAt(1)) { + case '-': + // Immediate string + buf = arg.substring(2).getBytes(Charset.forName("UTF-8")); + break; + + case '$': + // Environment variable + buf = System.getenv(arg.substring(2)).getBytes(Charset.forName("UTF-8")); + break; + + default: + buf = registers.get(arg.charAt(1)); + break; + } + + stream.write(buf, 0, buf.length); +} + + public static void main(String[] args) throws Exception { + for (String arg : args) { + switch (arg.charAt(0)) { + case 'I': + char register = arg.charAt(1); + int length = Integer.parseInt(arg.substring(2)); + byte[] buf = new byte[length]; + registers.put(register, buf); + System.in.read(buf, 0, length); + break; + + case 'E': + writeBytes(System.err, arg); + break; + + case 'O': + writeBytes(System.out, arg); + break; + + case 'X': + System.exit(Integer.parseInt(arg.substring(1))); + } + } + } +} diff --git a/src/test/java/com/google/devtools/build/lib/windows/WindowsProcessesTest.java b/src/test/java/com/google/devtools/build/lib/windows/WindowsProcessesTest.java new file mode 100644 index 0000000000..ecd7c26229 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/windows/WindowsProcessesTest.java @@ -0,0 +1,358 @@ +// Copyright 2016 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.devtools.build.lib.windows; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.util.concurrent.Uninterruptibles; +import com.google.devtools.build.lib.testutil.TestSpec; +import com.google.devtools.build.lib.util.OS; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Unit tests for {@link WindowsProcesses}. + */ +@RunWith(JUnit4.class) +@TestSpec(localOnly = true, supportedOs = OS.WINDOWS) +public class WindowsProcessesTest { + private static final Charset UTF8 = Charset.forName("UTF-8"); + private String mockSubprocess; + private String javaHome; + private long process; + + @Before + public void loadJni() throws Exception { + String jniDllPath = WindowsTestUtil.getRunfile("io_bazel/src/main/native/windows_jni.dll"); + mockSubprocess = WindowsTestUtil.getRunfile( + "io_bazel/src/test/java/com/google/devtools/build/lib/MockSubprocess_deploy.jar"); + javaHome = System.getProperty("java.home"); + + WindowsJniLoader.loadJniForTesting(jniDllPath); + + process = -1; + } + + @After + public void terminateProcess() throws Exception { + if (process != -1) { + WindowsProcesses.nativeTerminate(process); + WindowsProcesses.nativeDelete(process); + process = -1; + } + } + private String mockArgs(String... args) { + List<String> argv = new ArrayList<>(); + + argv.add(javaHome + "/bin/java"); + argv.add("-jar"); + argv.add(mockSubprocess); + for (String arg : args) { + argv.add(arg); + } + + return WindowsProcesses.quoteCommandLine(argv); + } + + private void assertNoError() throws Exception { + assertThat(WindowsProcesses.nativeGetLastError(process)).isEmpty(); + } + + @Test + public void testSmoke() throws Exception { + process = WindowsProcesses.nativeCreateProcess(mockArgs("Ia5", "Oa"), null, null, null); + assertNoError(); + + byte[] input = "HELLO".getBytes(UTF8); + byte[] output = new byte[5]; + WindowsProcesses.nativeWriteStdin(process, input, 0, 5); + assertNoError(); + WindowsProcesses.nativeReadStdout(process, output, 0, 5); + assertNoError(); + assertThat(new String(output, UTF8)).isEqualTo("HELLO"); + } + + @Test + public void testPingpong() throws Exception { + List<String> args = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + args.add("Ia3"); + args.add("Oa"); + } + + process = WindowsProcesses.nativeCreateProcess(mockArgs(args.toArray(new String[] {})), null, + null, null); + for (int i = 0; i < 100; i++) { + byte[] input = String.format("%03d", i).getBytes(UTF8); + assertThat(input.length).isEqualTo(3); + assertThat(WindowsProcesses.nativeWriteStdin(process, input, 0, 3)).isEqualTo(3); + byte[] output = new byte[3]; + assertThat(WindowsProcesses.nativeReadStdout(process, output, 0, 3)).isEqualTo(3); + assertThat(Integer.parseInt(new String(output, UTF8))).isEqualTo(i); + } + } + + private void startInterruptThread(final long delayMilliseconds) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + while (true) { + Uninterruptibles.sleepUninterruptibly(delayMilliseconds, TimeUnit.MILLISECONDS); + WindowsProcesses.nativeInterrupt(process); + } + } + }); + + thread.setDaemon(true); + thread.start(); + } + + @Test + public void testInterruption() throws Exception { + process = WindowsProcesses.nativeCreateProcess(mockArgs("Ia1"), null, null, null); // hang + startInterruptThread(1000); + // If the interruption doesn't work, this will hang indefinitely, but there isn't a lot + // we can do in that case because we can't just tell native code to stop whatever it's doing + // from Java. + assertThat(WindowsProcesses.nativeWaitFor(process)).isEqualTo(-1); + assertThat(WindowsProcesses.nativeIsInterrupted(process)).isTrue(); + } + + @Test + public void testExitCode() throws Exception { + process = WindowsProcesses.nativeCreateProcess(mockArgs("X42"), null, null, null); + assertThat(WindowsProcesses.nativeWaitFor(process)).isEqualTo(42); + assertNoError(); + } + + @Test + public void testPartialRead() throws Exception { + process = WindowsProcesses.nativeCreateProcess(mockArgs("O-HELLO"), null, null, null); + byte[] one = new byte[2]; + byte[] two = new byte[3]; + + assertThat(WindowsProcesses.nativeReadStdout(process, one, 0, 2)).isEqualTo(2); + assertNoError(); + assertThat(WindowsProcesses.nativeReadStdout(process, two, 0, 3)).isEqualTo(3); + assertNoError(); + + assertThat(new String(one, UTF8)).isEqualTo("HE"); + assertThat(new String(two, UTF8)).isEqualTo("LLO"); + } + + @Test + public void testArrayOutOfBounds() throws Exception { + process = WindowsProcesses.nativeCreateProcess(mockArgs("O-oob"), null, null, null); + byte[] buf = new byte[3]; + assertThat(WindowsProcesses.nativeReadStdout(process, buf, -1, 3)).isEqualTo(-1); + assertThat(WindowsProcesses.nativeReadStdout(process, buf, 0, 5)).isEqualTo(-1); + assertThat(WindowsProcesses.nativeReadStdout(process, buf, 4, 1)).isEqualTo(-1); + assertThat(WindowsProcesses.nativeReadStdout(process, buf, 2, -1)).isEqualTo(-1); + assertThat(WindowsProcesses.nativeReadStdout(process, buf, Integer.MAX_VALUE, 2)) + .isEqualTo(-1); + assertThat(WindowsProcesses.nativeReadStdout(process, buf, 2, Integer.MAX_VALUE)) + .isEqualTo(-1); + assertThat(WindowsProcesses.nativeReadStderr(process, buf, -1, 3)).isEqualTo(-1); + assertThat(WindowsProcesses.nativeReadStderr(process, buf, 0, 5)).isEqualTo(-1); + assertThat(WindowsProcesses.nativeReadStderr(process, buf, 4, 1)).isEqualTo(-1); + assertThat(WindowsProcesses.nativeReadStderr(process, buf, 2, -1)).isEqualTo(-1); + assertThat(WindowsProcesses.nativeReadStderr(process, buf, Integer.MAX_VALUE, 2)) + .isEqualTo(-1); + assertThat(WindowsProcesses.nativeReadStderr(process, buf, 2, Integer.MAX_VALUE)) + .isEqualTo(-1); + assertThat(WindowsProcesses.nativeWriteStdin(process, buf, -1, 3)).isEqualTo(-1); + assertThat(WindowsProcesses.nativeWriteStdin(process, buf, 0, 5)).isEqualTo(-1); + assertThat(WindowsProcesses.nativeWriteStdin(process, buf, 4, 1)).isEqualTo(-1); + assertThat(WindowsProcesses.nativeWriteStdin(process, buf, 2, -1)).isEqualTo(-1); + assertThat(WindowsProcesses.nativeWriteStdin(process, buf, Integer.MAX_VALUE, 2)) + .isEqualTo(-1); + assertThat(WindowsProcesses.nativeWriteStdin(process, buf, 2, Integer.MAX_VALUE)) + .isEqualTo(-1); + + assertThat(WindowsProcesses.nativeReadStdout(process, buf, 0, 3)).isEqualTo(3); + assertThat(new String(buf, UTF8)).isEqualTo("oob"); + } + + @Test + public void testOffsetedOps() throws Exception { + process = WindowsProcesses.nativeCreateProcess(mockArgs("Ia3", "Oa"), null, null, null); + byte[] input = "01234".getBytes(UTF8); + byte[] output = "abcde".getBytes(UTF8); + + assertThat(WindowsProcesses.nativeWriteStdin(process, input, 1, 3)).isEqualTo(3); + assertNoError(); + int rv = WindowsProcesses.nativeReadStdout(process, output, 1, 3); + assertNoError(); + assertThat(rv).isEqualTo(3); + + assertThat(new String(output, UTF8)).isEqualTo("a123e"); + } + + @Test + public void testParallelStdoutAndStderr() throws Exception { + process = WindowsProcesses.nativeCreateProcess(mockArgs( + "O-out1", "E-err1", "O-out2", "E-err2", "E-err3", "O-out3", "E-err4", "O-out4"), + null, null, null); + + byte[] buf = new byte[4]; + assertThat(WindowsProcesses.nativeReadStdout(process, buf, 0, 4)).isEqualTo(4); + assertThat(new String(buf, UTF8)).isEqualTo("out1"); + assertThat(WindowsProcesses.nativeReadStderr(process, buf, 0, 4)).isEqualTo(4); + assertThat(new String(buf, UTF8)).isEqualTo("err1"); + + assertThat(WindowsProcesses.nativeReadStderr(process, buf, 0, 4)).isEqualTo(4); + assertThat(new String(buf, UTF8)).isEqualTo("err2"); + assertThat(WindowsProcesses.nativeReadStdout(process, buf, 0, 4)).isEqualTo(4); + assertThat(new String(buf, UTF8)).isEqualTo("out2"); + + assertThat(WindowsProcesses.nativeReadStdout(process, buf, 0, 4)).isEqualTo(4); + assertThat(new String(buf, UTF8)).isEqualTo("out3"); + assertThat(WindowsProcesses.nativeReadStderr(process, buf, 0, 4)).isEqualTo(4); + assertThat(new String(buf, UTF8)).isEqualTo("err3"); + + assertThat(WindowsProcesses.nativeReadStderr(process, buf, 0, 4)).isEqualTo(4); + assertThat(new String(buf, UTF8)).isEqualTo("err4"); + assertThat(WindowsProcesses.nativeReadStdout(process, buf, 0, 4)).isEqualTo(4); + assertThat(new String(buf, UTF8)).isEqualTo("out4"); + } + + @Test + public void testExecutableNotFound() throws Exception { + process = WindowsProcesses.nativeCreateProcess("ThisExecutableDoesNotExist", null, null, null); + assertThat(WindowsProcesses.nativeGetLastError(process)) + .contains("The system cannot find the file specified."); + byte[] buf = new byte[1]; + assertThat(WindowsProcesses.nativeReadStdout(process, buf, 0, 1)).isEqualTo(-1); + } + + @Test + public void testReadingAndWritingAfterTermination() throws Exception { + process = WindowsProcesses.nativeCreateProcess("X42", null, null, null); + byte[] buf = new byte[1]; + assertThat(WindowsProcesses.nativeReadStdout(process, buf, 0, 1)).isEqualTo(-1); + assertThat(WindowsProcesses.nativeReadStderr(process, buf, 0, 1)).isEqualTo(-1); + assertThat(WindowsProcesses.nativeWriteStdin(process, buf, 0, 1)).isEqualTo(-1); + } + + @Test + public void testNewEnvironmentVariables() throws Exception { + byte[] data = "ONE=one\0TWO=twotwo\0\0".getBytes(UTF8); + process = WindowsProcesses.nativeCreateProcess(mockArgs("O$ONE", "O$TWO"), data, null, null); + assertNoError(); + byte[] buf = new byte[3]; + assertThat(WindowsProcesses.nativeReadStdout(process, buf, 0, 3)).isEqualTo(3); + assertThat(new String(buf, UTF8)).isEqualTo("one"); + buf = new byte[6]; + assertThat(WindowsProcesses.nativeReadStdout(process, buf, 0, 6)).isEqualTo(6); + assertThat(new String(buf, UTF8)).isEqualTo("twotwo"); + } + + @Test + public void testNoZeroInEnvBuffer() throws Exception { + byte[] data = "clown".getBytes(UTF8); + process = WindowsProcesses.nativeCreateProcess(mockArgs(), data, null, null); + assertThat(WindowsProcesses.nativeGetLastError(process)).isNotEmpty(); + } + + @Test + public void testOneZeroInEnvBuffer() throws Exception { + byte[] data = "FOO=bar\0".getBytes(UTF8); + process = WindowsProcesses.nativeCreateProcess(mockArgs(), data, null, null); + assertThat(WindowsProcesses.nativeGetLastError(process)).isNotEmpty(); + } + + @Test + public void testOneByteEnvBuffer() throws Exception { + byte[] data = "a".getBytes(UTF8); + process = WindowsProcesses.nativeCreateProcess(mockArgs(), data, null, null); + assertThat(WindowsProcesses.nativeGetLastError(process)).isNotEmpty(); + } + + @Test + public void testRedirect() throws Exception { + String stdoutFile = System.getenv("TEST_TMPDIR") + "\\stdout_redirect"; + String stderrFile = System.getenv("TEST_TMPDIR") + "\\stderr_redirect"; + + process = WindowsProcesses.nativeCreateProcess(mockArgs("O-one", "E-two"), + null, stdoutFile, stderrFile); + assertThat(process).isGreaterThan(0L); + assertNoError(); + WindowsProcesses.nativeWaitFor(process); + assertNoError(); + byte[] stdout = Files.readAllBytes(Paths.get(stdoutFile)); + byte[] stderr = Files.readAllBytes(Paths.get(stderrFile)); + assertThat(new String(stdout, UTF8)).isEqualTo("one"); + assertThat(new String(stderr, UTF8)).isEqualTo("two"); + } + + @Test + public void testRedirectToSameFile() throws Exception { + String file = System.getenv("TEST_TMPDIR") + "\\captured_"; + + process = WindowsProcesses.nativeCreateProcess(mockArgs("O-one", "E-two"), + null, file, file); + assertThat(process).isGreaterThan(0L); + assertNoError(); + WindowsProcesses.nativeWaitFor(process); + assertNoError(); + byte[] bytes = Files.readAllBytes(Paths.get(file)); + assertThat(new String(bytes, UTF8)).isEqualTo("onetwo"); + } + + @Test + public void testErrorWhenReadingFromRedirectedStreams() throws Exception { + String stdoutFile = System.getenv("TEST_TMPDIR") + "\\captured_stdout"; + String stderrFile = System.getenv("TEST_TMPDIR") + "\\captured_stderr"; + + process = WindowsProcesses.nativeCreateProcess(mockArgs("O-one", "E-two"), null, + stdoutFile, stderrFile); + assertNoError(); + byte[] buf = new byte[1]; + assertThat(WindowsProcesses.nativeReadStdout(process, buf, 0, 1)).isEqualTo(-1); + assertThat(WindowsProcesses.nativeReadStderr(process, buf, 0, 1)).isEqualTo(-1); + WindowsProcesses.nativeWaitFor(process); + } + + @Test + public void testAppendToExistingFile() throws Exception { + String stdoutFile = System.getenv("TEST_TMPDIR") + "\\stdout_atef"; + String stderrFile = System.getenv("TEST_TMPDIR") + "\\stderr_atef"; + Path stdout = Paths.get(stdoutFile); + Path stderr = Paths.get(stderrFile); + Files.write(stdout, "out1".getBytes(UTF8)); + Files.write(stderr, "err1".getBytes(UTF8)); + + process = WindowsProcesses.nativeCreateProcess(mockArgs("O-out2", "E-err2"), null, + stdoutFile, stderrFile); + assertNoError(); + WindowsProcesses.nativeWaitFor(process); + assertNoError(); + byte[] stdoutBytes = Files.readAllBytes(Paths.get(stdoutFile)); + byte[] stderrBytes = Files.readAllBytes(Paths.get(stderrFile)); + assertThat(new String(stdoutBytes, UTF8)).isEqualTo("out1out2"); + assertThat(new String(stderrBytes, UTF8)).isEqualTo("err1err2"); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/windows/WindowsTestUtil.java b/src/test/java/com/google/devtools/build/lib/windows/WindowsTestUtil.java new file mode 100644 index 0000000000..6440d78134 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/windows/WindowsTestUtil.java @@ -0,0 +1,55 @@ +// Copyright 2016 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.devtools.build.lib.windows; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +/** + * Utilities for running Java tests on Windows. + */ +public class WindowsTestUtil { + private static Map<String, String> runfiles; + public static String getRunfile(String runfilesPath) throws IOException { + ensureRunfilesParsed(); + return runfiles.get(runfilesPath); + } + + private static synchronized void ensureRunfilesParsed() throws IOException { + if (runfiles != null) { + return; + } + + runfiles = new HashMap<>(); + InputStream fis = new FileInputStream(System.getenv("RUNFILES_MANIFEST_FILE")); + InputStreamReader isr = new InputStreamReader(fis, Charset.forName("UTF-8")); + BufferedReader br = new BufferedReader(isr); + String line; + while ((line = br.readLine()) != null) { + String[] splitLine = line.split(" "); // This is buggy when the path contains spaces + if (splitLine.length != 2) { + continue; + } + + runfiles.put(splitLine[0], splitLine[1]); + } + } +} |