/* * Copyright 2015-present Facebook, Inc. * * 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.facebook.buck.parser; import static com.facebook.buck.parser.ParserConfig.DEFAULT_BUILD_FILE_NAME; import static org.junit.Assert.assertThat; import static org.junit.Assume.assumeTrue; import com.facebook.buck.event.BuckEventBus; import com.facebook.buck.event.BuckEventBusFactory; import com.facebook.buck.event.ConsoleEvent; import com.facebook.buck.io.WatchmanDiagnosticEvent; import com.facebook.buck.json.BuildFileParseException; import com.facebook.buck.json.ProjectBuildFileParser; import com.facebook.buck.json.ProjectBuildFileParserOptions; import com.facebook.buck.rules.Cell; import com.facebook.buck.rules.KnownBuildRuleTypes; import com.facebook.buck.rules.TestCellBuilder; import com.facebook.buck.rules.coercer.DefaultTypeCoercerFactory; import com.facebook.buck.testutil.TestConsole; import com.facebook.buck.timing.FakeClock; import com.facebook.buck.util.FakeProcess; import com.facebook.buck.util.FakeProcessExecutor; import com.facebook.buck.util.ObjectMappers; import com.facebook.buck.util.ProcessExecutor; import com.facebook.buck.util.environment.Platform; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.eventbus.Subscribe; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.atomic.AtomicLong; import org.hamcrest.Matchers; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; public class ProjectBuildFileParserTest { private Cell cell; @Rule public ExpectedException thrown = ExpectedException.none(); @Before public void createCell() throws IOException, InterruptedException { cell = new TestCellBuilder().build(); } private static FakeProcess fakeProcessWithJsonOutput( int returnCode, List<Object> values, Optional<List<Object>> diagnostics, Optional<String> stdout) { Map<String, Object> outputToSerialize = new LinkedHashMap<>(); outputToSerialize.put("values", values); if (diagnostics.isPresent()) { outputToSerialize.put("diagnostics", diagnostics.get()); } byte[] serialized; try { serialized = ObjectMappers.WRITER.writeValueAsBytes(outputToSerialize); } catch (IOException e) { throw new RuntimeException(e); } return new FakeProcess( returnCode, new ByteArrayOutputStream(), new ByteArrayInputStream(serialized), new ByteArrayInputStream(stdout.orElse("").getBytes(StandardCharsets.UTF_8))); } @Test public void whenSubprocessReturnsSuccessThenProjectBuildFileParserClosesCleanly() throws IOException, BuildFileParseException, InterruptedException { TestProjectBuildFileParserFactory buildFileParserFactory = new TestProjectBuildFileParserFactory(cell.getRoot(), cell.getKnownBuildRuleTypes()); try (ProjectBuildFileParser buildFileParser = buildFileParserFactory.createNoopParserThatAlwaysReturnsSuccess()) { buildFileParser.initIfNeeded(); // close() is called implicitly at the end of this block. It must not throw. } } @Test(expected = BuildFileParseException.class) public void whenSubprocessReturnsFailureThenProjectBuildFileParserThrowsOnClose() throws IOException, BuildFileParseException, InterruptedException { TestProjectBuildFileParserFactory buildFileParserFactory = new TestProjectBuildFileParserFactory(cell.getRoot(), cell.getKnownBuildRuleTypes()); try (ProjectBuildFileParser buildFileParser = buildFileParserFactory.createNoopParserThatAlwaysReturnsError()) { buildFileParser.initIfNeeded(); // close() is called implicitly at the end of this block. It must throw. } } @Test public void whenSubprocessPrintsWarningToStderrThenConsoleEventPublished() throws IOException, BuildFileParseException, InterruptedException { // This test depends on unix utilities that don't exist on Windows. assumeTrue(Platform.detect() != Platform.WINDOWS); TestProjectBuildFileParserFactory buildFileParserFactory = new TestProjectBuildFileParserFactory(cell.getRoot(), cell.getKnownBuildRuleTypes()); BuckEventBus buckEventBus = BuckEventBusFactory.newInstance(new FakeClock(0)); final List<ConsoleEvent> consoleEvents = new ArrayList<>(); class EventListener { @Subscribe public void onConsoleEvent(ConsoleEvent consoleEvent) { consoleEvents.add(consoleEvent); } } EventListener eventListener = new EventListener(); buckEventBus.register(eventListener); try (ProjectBuildFileParser buildFileParser = buildFileParserFactory.createNoopParserThatAlwaysReturnsSuccessAndPrintsToStderr( buckEventBus)) { buildFileParser.initIfNeeded(); buildFileParser.getAllRulesAndMetaRules(Paths.get("foo"), new AtomicLong()); } assertThat( consoleEvents, Matchers.contains( Matchers.hasToString("Warning raised by BUCK file parser: Don't Panic!"))); } @Test public void whenSubprocessReturnsWarningThenConsoleEventPublished() throws IOException, BuildFileParseException, InterruptedException { // This test depends on unix utilities that don't exist on Windows. assumeTrue(Platform.detect() != Platform.WINDOWS); TestProjectBuildFileParserFactory buildFileParserFactory = new TestProjectBuildFileParserFactory(cell.getRoot(), cell.getKnownBuildRuleTypes()); BuckEventBus buckEventBus = BuckEventBusFactory.newInstance(new FakeClock(0)); final List<ConsoleEvent> consoleEvents = new ArrayList<>(); final List<WatchmanDiagnosticEvent> watchmanDiagnosticEvents = new ArrayList<>(); class EventListener { @Subscribe public void on(ConsoleEvent consoleEvent) { consoleEvents.add(consoleEvent); } @Subscribe public void on(WatchmanDiagnosticEvent event) { watchmanDiagnosticEvents.add(event); } } EventListener eventListener = new EventListener(); buckEventBus.register(eventListener); try (ProjectBuildFileParser buildFileParser = buildFileParserFactory.createNoopParserThatAlwaysReturnsSuccessWithWarning( buckEventBus, "This is a warning", "parser")) { buildFileParser.initIfNeeded(); buildFileParser.getAllRulesAndMetaRules(Paths.get("foo"), new AtomicLong()); } assertThat( consoleEvents, Matchers.contains( Matchers.hasToString("Warning raised by BUCK file parser: This is a warning"))); assertThat( "Should not receive any watchman diagnostic events", watchmanDiagnosticEvents, Matchers.empty()); } @Test public void whenSubprocessReturnsNewWatchmanWarningThenDiagnosticEventPublished() throws IOException, BuildFileParseException, InterruptedException { // This test depends on unix utilities that don't exist on Windows. assumeTrue(Platform.detect() != Platform.WINDOWS); TestProjectBuildFileParserFactory buildFileParserFactory = new TestProjectBuildFileParserFactory(cell.getRoot(), cell.getKnownBuildRuleTypes()); BuckEventBus buckEventBus = BuckEventBusFactory.newInstance(new FakeClock(0)); final List<WatchmanDiagnosticEvent> watchmanDiagnosticEvents = new ArrayList<>(); class EventListener { @Subscribe public void on(WatchmanDiagnosticEvent consoleEvent) { watchmanDiagnosticEvents.add(consoleEvent); } } EventListener eventListener = new EventListener(); buckEventBus.register(eventListener); try (ProjectBuildFileParser buildFileParser = buildFileParserFactory.createNoopParserThatAlwaysReturnsSuccessWithWarning( buckEventBus, "This is a watchman warning", "watchman")) { buildFileParser.initIfNeeded(); buildFileParser.getAllRulesAndMetaRules(Paths.get("foo"), new AtomicLong()); } assertThat( watchmanDiagnosticEvents, Matchers.contains( Matchers.hasToString(Matchers.containsString("This is a watchman warning")))); } @Test public void whenSubprocessReturnsErrorThenConsoleEventPublished() throws IOException, BuildFileParseException, InterruptedException { // This test depends on unix utilities that don't exist on Windows. assumeTrue(Platform.detect() != Platform.WINDOWS); TestProjectBuildFileParserFactory buildFileParserFactory = new TestProjectBuildFileParserFactory(cell.getRoot(), cell.getKnownBuildRuleTypes()); BuckEventBus buckEventBus = BuckEventBusFactory.newInstance(new FakeClock(0)); final List<ConsoleEvent> consoleEvents = new ArrayList<>(); class EventListener { @Subscribe public void onConsoleEvent(ConsoleEvent consoleEvent) { consoleEvents.add(consoleEvent); } } EventListener eventListener = new EventListener(); buckEventBus.register(eventListener); try (ProjectBuildFileParser buildFileParser = buildFileParserFactory.createNoopParserThatAlwaysReturnsSuccessWithError( buckEventBus, "This is an error", "parser")) { buildFileParser.initIfNeeded(); buildFileParser.getAllRulesAndMetaRules(Paths.get("foo"), new AtomicLong()); } assertThat( consoleEvents, Matchers.contains( Matchers.hasToString("Error raised by BUCK file parser: This is an error"))); } @Test public void whenSubprocessReturnsSyntaxErrorInFileBeingParsedThenExceptionContainsFileNameOnce() throws IOException, BuildFileParseException, InterruptedException { // This test depends on unix utilities that don't exist on Windows. assumeTrue(Platform.detect() != Platform.WINDOWS); TestProjectBuildFileParserFactory buildFileParserFactory = new TestProjectBuildFileParserFactory(cell.getRoot(), cell.getKnownBuildRuleTypes()); thrown.expect(BuildFileParseException.class); thrown.expectMessage( "Parse error for build file foo/BUCK:\n" + "Syntax error on line 23, column 16:\n" + "java_test(name=*@!&#(!@&*()\n" + " ^"); try (ProjectBuildFileParser buildFileParser = buildFileParserFactory.createNoopParserThatAlwaysReturnsErrorWithException( BuckEventBusFactory.newInstance(new FakeClock(0)), "This is an error", "parser", ImmutableMap.<String, Object>builder() .put("type", "SyntaxError") .put("value", "You messed up") .put("filename", "foo/BUCK") .put("lineno", 23) .put("offset", 16) .put("text", "java_test(name=*@!&#(!@&*()\n") .put( "traceback", ImmutableList.of( ImmutableMap.of( "filename", "foo/BUCK", "line_number", 23, "function_name", "<module>", "text", "java_test(name=*@!&#(!@&*()\n"))) .build())) { buildFileParser.initIfNeeded(); buildFileParser.getAllRulesAndMetaRules(Paths.get("foo/BUCK"), new AtomicLong()); } } @Test public void whenSubprocessReturnsSyntaxErrorWithoutOffsetThenExceptionIsFormattedWithoutColumn() throws IOException, BuildFileParseException, InterruptedException { // This test depends on unix utilities that don't exist on Windows. assumeTrue(Platform.detect() != Platform.WINDOWS); TestProjectBuildFileParserFactory buildFileParserFactory = new TestProjectBuildFileParserFactory(cell.getRoot(), cell.getKnownBuildRuleTypes()); thrown.expect(BuildFileParseException.class); thrown.expectMessage( "Parse error for build file foo/BUCK:\n" + "Syntax error on line 23:\n" + "java_test(name=*@!&#(!@&*()"); try (ProjectBuildFileParser buildFileParser = buildFileParserFactory.createNoopParserThatAlwaysReturnsErrorWithException( BuckEventBusFactory.newInstance(new FakeClock(0)), "This is an error", "parser", ImmutableMap.<String, Object>builder() .put("type", "SyntaxError") .put("value", "You messed up") .put("filename", "foo/BUCK") .put("lineno", 23) .put("text", "java_test(name=*@!&#(!@&*()\n") .put( "traceback", ImmutableList.of( ImmutableMap.of( "filename", "foo/BUCK", "line_number", 23, "function_name", "<module>", "text", "java_test(name=*@!&#(!@&*()\n"))) .build())) { buildFileParser.initIfNeeded(); buildFileParser.getAllRulesAndMetaRules(Paths.get("foo/BUCK"), new AtomicLong()); } } @Test public void whenSubprocessReturnsSyntaxErrorInDifferentFileThenExceptionContainsTwoFileNames() throws IOException, BuildFileParseException, InterruptedException { // This test depends on unix utilities that don't exist on Windows. assumeTrue(Platform.detect() != Platform.WINDOWS); TestProjectBuildFileParserFactory buildFileParserFactory = new TestProjectBuildFileParserFactory(cell.getRoot(), cell.getKnownBuildRuleTypes()); BuckEventBus buckEventBus = BuckEventBusFactory.newInstance(new FakeClock(0)); final List<ConsoleEvent> consoleEvents = new ArrayList<>(); class EventListener { @Subscribe public void onConsoleEvent(ConsoleEvent consoleEvent) { consoleEvents.add(consoleEvent); } } EventListener eventListener = new EventListener(); buckEventBus.register(eventListener); thrown.expect(BuildFileParseException.class); thrown.expectMessage( "Parse error for build file foo/BUCK:\n" + "Syntax error in bar/BUCK\n" + "Line 42, column 24:\n" + "def some_helper_method(!@87*@!#\n" + " ^"); try (ProjectBuildFileParser buildFileParser = buildFileParserFactory.createNoopParserThatAlwaysReturnsErrorWithException( buckEventBus, "This is an error", "parser", ImmutableMap.<String, Object>builder() .put("type", "SyntaxError") .put("value", "You messed up") .put("filename", "bar/BUCK") .put("lineno", 42) .put("offset", 24) .put("text", "def some_helper_method(!@87*@!#\n") .put( "traceback", ImmutableList.of( ImmutableMap.of( "filename", "bar/BUCK", "line_number", 42, "function_name", "<module>", "text", "def some_helper_method(!@87*@!#\n"), ImmutableMap.of( "filename", "foo/BUCK", "line_number", 23, "function_name", "<module>", "text", "some_helper_method(name=*@!&#(!@&*()\n"))) .build())) { buildFileParser.initIfNeeded(); buildFileParser.getAllRulesAndMetaRules(Paths.get("foo/BUCK"), new AtomicLong()); } } @Test public void whenSubprocessReturnsNonSyntaxErrorThenExceptionContainsFullStackTrace() throws IOException, BuildFileParseException, InterruptedException { // This test depends on unix utilities that don't exist on Windows. assumeTrue(Platform.detect() != Platform.WINDOWS); TestProjectBuildFileParserFactory buildFileParserFactory = new TestProjectBuildFileParserFactory(cell.getRoot(), cell.getKnownBuildRuleTypes()); thrown.expect(BuildFileParseException.class); thrown.expectMessage( "Parse error for build file foo/BUCK:\n" + "ZeroDivisionError: integer division or modulo by zero\n" + "Call stack:\n" + " File \"bar/BUCK\", line 42, in lets_divide_by_zero\n" + " foo / bar\n" + "\n" + " File \"foo/BUCK\", line 23\n" + " lets_divide_by_zero()\n" + "\n"); try (ProjectBuildFileParser buildFileParser = buildFileParserFactory.createNoopParserThatAlwaysReturnsErrorWithException( BuckEventBusFactory.newInstance(new FakeClock(0)), "This is an error", "parser", ImmutableMap.<String, Object>builder() .put("type", "ZeroDivisionError") .put("value", "integer division or modulo by zero") .put("filename", "bar/BUCK") .put("lineno", 42) .put("offset", 24) .put("text", "foo / bar\n") .put( "traceback", ImmutableList.of( ImmutableMap.of( "filename", "bar/BUCK", "line_number", 42, "function_name", "lets_divide_by_zero", "text", "foo / bar\n"), ImmutableMap.of( "filename", "foo/BUCK", "line_number", 23, "function_name", "<module>", "text", "lets_divide_by_zero()\n"))) .build())) { buildFileParser.initIfNeeded(); buildFileParser.getAllRulesAndMetaRules(Paths.get("foo/BUCK"), new AtomicLong()); } } /** * ProjectBuildFileParser test double which counts the number of times rules are parsed to test * caching logic in Parser. */ private static class TestProjectBuildFileParserFactory { private final Path projectRoot; private final KnownBuildRuleTypes buildRuleTypes; public TestProjectBuildFileParserFactory(Path projectRoot, KnownBuildRuleTypes buildRuleTypes) { this.projectRoot = projectRoot; this.buildRuleTypes = buildRuleTypes; } public ProjectBuildFileParser createNoopParserThatAlwaysReturnsError() { return new TestProjectBuildFileParser( "fake-python", new FakeProcessExecutor( params -> fakeProcessWithJsonOutput( 1, ImmutableList.of(), Optional.empty(), Optional.empty()), new TestConsole()), BuckEventBusFactory.newInstance()); } public ProjectBuildFileParser createNoopParserThatAlwaysReturnsSuccess() { return new TestProjectBuildFileParser( "fake-python", new FakeProcessExecutor( params -> fakeProcessWithJsonOutput( 0, ImmutableList.of(), Optional.empty(), Optional.empty()), new TestConsole()), BuckEventBusFactory.newInstance()); } public ProjectBuildFileParser createNoopParserThatAlwaysReturnsSuccessAndPrintsToStderr( BuckEventBus buckEventBus) { return new TestProjectBuildFileParser( "fake-python", new FakeProcessExecutor( params -> fakeProcessWithJsonOutput( 0, ImmutableList.of(), Optional.empty(), Optional.of("Don't Panic!")), new TestConsole()), buckEventBus); } public ProjectBuildFileParser createNoopParserThatAlwaysReturnsSuccessWithWarning( BuckEventBus buckEventBus, final String warning, final String source) { return new TestProjectBuildFileParser( "fake-python", new FakeProcessExecutor( params -> fakeProcessWithJsonOutput( 0, ImmutableList.of(), Optional.of( ImmutableList.of( ImmutableMap.of( "level", "warning", "message", warning, "source", source))), Optional.empty()), new TestConsole()), buckEventBus); } public ProjectBuildFileParser createNoopParserThatAlwaysReturnsSuccessWithError( BuckEventBus buckEventBus, final String error, final String source) { return new TestProjectBuildFileParser( "fake-python", new FakeProcessExecutor( params -> fakeProcessWithJsonOutput( 0, ImmutableList.of(), Optional.of( ImmutableList.of( ImmutableMap.of( "level", "error", "message", error, "source", source))), Optional.empty()), new TestConsole()), buckEventBus); } public ProjectBuildFileParser createNoopParserThatAlwaysReturnsErrorWithException( BuckEventBus buckEventBus, final String error, final String source, final ImmutableMap<String, Object> exception) { return new TestProjectBuildFileParser( "fake-python", new FakeProcessExecutor( params -> fakeProcessWithJsonOutput( 1, ImmutableList.of(), Optional.of( ImmutableList.of( ImmutableMap.of( "level", "fatal", "message", error, "source", source, "exception", exception))), Optional.empty()), new TestConsole()), buckEventBus); } private class TestProjectBuildFileParser extends ProjectBuildFileParser { public TestProjectBuildFileParser( String pythonInterpreter, ProcessExecutor processExecutor, BuckEventBus buckEventBus) { super( ProjectBuildFileParserOptions.builder() .setProjectRoot(projectRoot) .setPythonInterpreter(pythonInterpreter) .setAllowEmptyGlobs(ParserConfig.DEFAULT_ALLOW_EMPTY_GLOBS) .setIgnorePaths(ImmutableSet.of()) .setBuildFileName(DEFAULT_BUILD_FILE_NAME) .setDefaultIncludes(ImmutableSet.of("//java/com/facebook/defaultIncludeFile")) .setDescriptions(buildRuleTypes.getAllDescriptions()) .setBuildFileImportWhitelist(ImmutableList.of()) .build(), new DefaultTypeCoercerFactory(), ImmutableMap.of(), buckEventBus, processExecutor); } } } }