// Copyright 2017 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.pkgcache; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.devtools.build.lib.analysis.BlazeDirectories; import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider; import com.google.devtools.build.lib.analysis.util.AnalysisMock; import com.google.devtools.build.lib.cmdline.PackageIdentifier; import com.google.devtools.build.lib.packages.NoSuchPackageException; import com.google.devtools.build.lib.packages.Package; import com.google.devtools.build.lib.skyframe.DiffAwareness; import com.google.devtools.build.lib.skyframe.PackageLookupFunction.CrossRepositoryLabelViolationStrategy; import com.google.devtools.build.lib.skyframe.PackageLookupValue.BuildFileName; import com.google.devtools.build.lib.skyframe.PrecomputedValue; import com.google.devtools.build.lib.skyframe.SequencedSkyframeExecutor; import com.google.devtools.build.lib.skyframe.SkyValueDirtinessChecker; import com.google.devtools.build.lib.skyframe.SkyframeExecutor; import com.google.devtools.build.lib.syntax.SkylarkSemanticsOptions; import com.google.devtools.build.lib.testutil.FoundationTestCase; import com.google.devtools.build.lib.testutil.ManualClock; import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor; import com.google.devtools.build.lib.vfs.FileSystem; import com.google.devtools.build.lib.vfs.FileSystemUtils; import com.google.devtools.build.lib.vfs.ModifiedFileSet; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.PathFragment; import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; import com.google.devtools.common.options.OptionsParser; import java.nio.charset.StandardCharsets; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * Tests for package loading. */ @RunWith(JUnit4.class) public class BuildFileModificationTest extends FoundationTestCase { private ManualClock clock = new ManualClock(); private AnalysisMock analysisMock; private ConfiguredRuleClassProvider ruleClassProvider; private SkyframeExecutor skyframeExecutor; @Before public final void disableLogging() throws Exception { Logger.getLogger("com.google.devtools").setLevel(Level.SEVERE); } @Before public final void initializeSkyframeExecutor() throws Exception { analysisMock = AnalysisMock.get(); ruleClassProvider = analysisMock.createRuleClassProvider(); BlazeDirectories directories = new BlazeDirectories(outputBase, outputBase, rootDirectory, analysisMock.getProductName()); skyframeExecutor = SequencedSkyframeExecutor.create( analysisMock .getPackageFactoryBuilderForTesting() .build(ruleClassProvider, scratch.getFileSystem()), directories, null, /* BinTools */ null, /* workspaceStatusActionFactory */ ruleClassProvider.getBuildInfoFactories(), ImmutableList.<DiffAwareness.Factory>of(), Predicates.<PathFragment>alwaysFalse(), AnalysisMock.get().getSkyFunctions(), ImmutableList.<PrecomputedValue.Injected>of(), ImmutableList.<SkyValueDirtinessChecker>of(), analysisMock.getProductName(), CrossRepositoryLabelViolationStrategy.ERROR, ImmutableList.of(BuildFileName.BUILD_DOT_BAZEL, BuildFileName.BUILD)); OptionsParser parser = OptionsParser.newOptionsParser( PackageCacheOptions.class, SkylarkSemanticsOptions.class); analysisMock.getInvocationPolicyEnforcer().enforce(parser); setUpSkyframe( parser.getOptions(PackageCacheOptions.class), parser.getOptions(SkylarkSemanticsOptions.class)); } private void setUpSkyframe( PackageCacheOptions packageCacheOptions, SkylarkSemanticsOptions skylarkSemanticsOptions) { PathPackageLocator pkgLocator = PathPackageLocator.create( null, packageCacheOptions.packagePath, reporter, rootDirectory, rootDirectory); packageCacheOptions.showLoadingProgress = true; packageCacheOptions.globbingThreads = 7; skyframeExecutor.preparePackageLoading( pkgLocator, packageCacheOptions, skylarkSemanticsOptions, analysisMock.getDefaultsPackageContent(), UUID.randomUUID(), ImmutableMap.<String, String>of(), ImmutableMap.<String, String>of(), new TimestampGranularityMonitor(clock)); skyframeExecutor.setDeletedPackages( ImmutableSet.copyOf(packageCacheOptions.getDeletedPackages())); } @Override protected FileSystem createFileSystem() { return new InMemoryFileSystem(clock); } private void invalidatePackages() throws InterruptedException { skyframeExecutor.invalidateFilesUnderPathForTesting( reporter, ModifiedFileSet.EVERYTHING_MODIFIED, rootDirectory); } private Package getPackage(String packageName) throws NoSuchPackageException, InterruptedException { return skyframeExecutor.getPackageManager().getPackage(reporter, PackageIdentifier.createInMainRepo(packageName)); } @Test public void testCTimeChangeDetectedWithError() throws Exception { reporter.removeHandler(failFastHandler); Path build = scratch.file( "a/BUILD", "cc_library(name='a', feet='stinky')".getBytes(StandardCharsets.ISO_8859_1)); Package a1 = getPackage("a"); assertTrue(a1.containsErrors()); assertContainsEvent("//a:a: no such attribute 'feet'"); eventCollector.clear(); // writeContent updates mtime and ctime. Note that we keep the content length exactly the same. clock.advanceMillis(1); FileSystemUtils.writeContent( build, "cc_library(name='a', srcs=['a.cc'])".getBytes(StandardCharsets.ISO_8859_1)); invalidatePackages(); Package a2 = getPackage("a"); assertNotSame(a1, a2); assertFalse(a2.containsErrors()); assertNoEvents(); } @Test public void testCTimeChangeDetected() throws Exception { Path path = scratch.file( "pkg/BUILD", "cc_library(name = 'foo')\n".getBytes(StandardCharsets.ISO_8859_1)); Package oldPkg = getPackage("pkg"); // Note that the content has exactly the same length as before. clock.advanceMillis(1); FileSystemUtils.writeContent( path, "cc_library(name = 'bar')\n".getBytes(StandardCharsets.ISO_8859_1)); assertSame(oldPkg, getPackage("pkg")); // Change only becomes visible after invalidatePackages. invalidatePackages(); Package newPkg = getPackage("pkg"); assertNotSame(oldPkg, newPkg); assertNotNull(newPkg.getTarget("bar")); } @Test public void testLengthChangeDetected() throws Exception { reporter.removeHandler(failFastHandler); Path build = scratch.file( "a/BUILD", "cc_library(name='a', srcs=['a.cc'])".getBytes(StandardCharsets.ISO_8859_1)); Package a1 = getPackage("a"); eventCollector.clear(); // Note that we didn't advance the clock, so ctime/mtime is the same as before. // However, the file contents are one byte longer. FileSystemUtils.writeContent( build, "cc_library(name='ab', srcs=['a.cc'])".getBytes(StandardCharsets.ISO_8859_1)); invalidatePackages(); Package a2 = getPackage("a"); assertNotSame(a1, a2); assertNoEvents(); } @Test public void testTouchedBuildFileCausesReloadAfterSync() throws Exception { Path path = scratch.file("pkg/BUILD", "cc_library(name = 'foo')"); Package oldPkg = getPackage("pkg"); // Change ctime to 1. clock.advanceMillis(1); path.setLastModifiedTime(1001); assertSame(oldPkg, getPackage("pkg")); // change not yet visible invalidatePackages(); Package newPkg = getPackage("pkg"); assertNotSame(oldPkg, newPkg); } }