blob: 21de6e95a2741f8c48fea19fd607270db5aa3c08 [file] [log] [blame]
// Copyright 2019 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.sandbox;
import static com.google.common.truth.Truth.assertThat;
import com.google.devtools.build.lib.vfs.DigestHashFunction;
import com.google.devtools.build.lib.vfs.FileSystem;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.JavaIoFileSystem;
import com.google.devtools.build.lib.vfs.Path;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.junit.After;
import org.junit.Before;
/**
* Common code to unit test {@link RealSandboxfsProcess}.
*
* <p>These tests validate the communication protocol between Bazel and a sandboxfs but do so using
* golden data. They are meant to sanity-check changes to the Bazel codebase against all supported
* sandboxfs versions but cannot guarantee that the integration with a real sandboxfs binary work.
*/
public abstract class BaseRealSandboxfsProcessTest {
/** Path to the mount point passed to sandboxfs. Not accessed, so it's not even created. */
static final String FAKE_MOUNT_POINT = "/non-existent/mount/point";
/** Sandboxfs version to return to Bazel when queried. */
private final String version;
/**
* Expected mount arguments for each {@link #createAndStartFakeSandboxfs} call. Supplied as a
* single string with all arguments concatenated as seen by sandboxfs.
*/
private final String expectedArgs;
private FileSystem fileSystem;
private Path tmpDir;
// Initialized via createAndStartFakeSandboxfs and checked by verifyFakeSandboxfsExecution.
private Path capturedArgs;
private Path capturedRequests;
BaseRealSandboxfsProcessTest(String version, String expectedArgs) {
this.version = version;
this.expectedArgs = expectedArgs;
}
@Before
public void setUp() throws Exception {
fileSystem = new JavaIoFileSystem(DigestHashFunction.getDefaultUnchecked());
tmpDir = fileSystem.getPath(System.getenv("TEST_TMPDIR")).getRelative("test");
tmpDir.createDirectory();
}
@After
public void tearDown() throws Exception {
tmpDir.deleteTree();
}
/**
* Starts a sandboxfs instance using a fake binary that captures all received requests and yields
* mock responses.
*
* @param responses the mock responses to return to Bazel when issuing requests, broken down by
* line printed to stdout
* @return a sandboxfs process handler
* @throws IOException if the fake sandboxfs cannot be prepared or started
*/
SandboxfsProcess createAndStartFakeSandboxfs(List<String> responses) throws IOException {
capturedArgs = tmpDir.getRelative("captured-args");
capturedRequests = tmpDir.getRelative("captured-requests");
Path fakeSandboxfs = tmpDir.getRelative("fake-sandboxfs");
try (PrintWriter writer =
new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(fakeSandboxfs.getOutputStream(), StandardCharsets.UTF_8)))) {
writer.println("#! /bin/bash");
// Ignore requests for termination. The real sandboxfs process must be sent a SIGTERM to stop
// serving, but in our case we want to terminate cleanly after waiting for all input to be
// recorded.
writer.println("trap '' TERM;");
// Handle a --version invocation and exit quickly, which is a prerequisite for the mount call.
writer.println("if [ \"${*}\" = \"--version\" ]; then");
writer.println(" echo sandboxfs " + version + ";");
writer.println(" exit 0;");
writer.println("fi;");
// Capture all arguments for later inspection.
writer.println("for arg in \"${@}\"; do");
writer.println(" echo \"${arg}\" >>" + capturedArgs + ";");
writer.println("done;");
// Attempt to "parse" requests coming through stdin by just counting brace pairs, assuming
// that the input is composed of a stream of JSON objects. Then, for each request, emit one
// response.
//
// We must do this because the unordered response processor required to parse 0.2.0 output
// expects responses to come only after their requests have been issued. Ideally we'd match
// our mock responses to specific requests to allow for testing of unordered responses, but
// for now assume all requests and responses in the test are correctly ordered.
//
// TODO(jmmv): This has become pretty awful. Should rethink unit testing.
for (String response : responses) {
writer.println("braces=0; started=no");
writer.println("while read -d '' -n 1 ch; do");
writer.println(" case \"${ch}\" in");
writer.println(" '{') braces=$((braces + 1)); started=yes ;;");
writer.println(" '[') braces=$((braces + 1)); started=yes ;;");
writer.println(" ']') braces=$((braces - 1)) ;;");
writer.println(" '}') braces=$((braces - 1)) ;;");
writer.println(" esac");
writer.println(" [[ \"${ch}\" != '' ]] || ch='\n'");
writer.println(" printf '%c' \"${ch}\" >>" + capturedRequests);
writer.println(" if [[ \"${started}\" = yes && \"${braces}\" -eq 0 ]]; then");
writer.println(" echo '" + response + "';");
writer.println(" break;");
writer.println(" fi");
writer.println("done");
}
// Capture any stray requests not expected by the test data.
writer.println("cat >>" + capturedRequests);
}
fakeSandboxfs.setExecutable(true);
return RealSandboxfsProcess.mount(
fakeSandboxfs.asFragment(),
fileSystem.getPath(FAKE_MOUNT_POINT),
tmpDir.getRelative("log"));
}
/**
* Checks that the given sandboxfs process behaved as expected.
*
* @param process the sandboxfs instance to stop and verify, which must have been previously
* started by {@link #createAndStartFakeSandboxfs}
* @param expectedRequests a flat string containing all requests given to sandboxfs (i.e. the raw
* contents of its stdin)
* @throws IOException if the fake sandboxfs instance cannot be stopped or if there is a problem
* reading the captured data
*/
void verifyFakeSandboxfsExecution(SandboxfsProcess process, String expectedRequests)
throws IOException {
process.destroy();
String args = FileSystemUtils.readContent(capturedArgs, StandardCharsets.UTF_8);
assertThat(args).isEqualTo(expectedArgs);
String requests = FileSystemUtils.readContent(capturedRequests, StandardCharsets.UTF_8);
assertThat(requests).isEqualTo(expectedRequests);
}
}