/* * Copyright 2016-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.cxx; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import com.facebook.buck.cli.FakeBuckConfig; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.model.BuildTargetFactory; import com.facebook.buck.rules.BuildRule; import com.facebook.buck.rules.BuildRuleParams; import com.facebook.buck.rules.BuildRuleResolver; import com.facebook.buck.rules.DefaultTargetNodeToBuildRuleTransformer; import com.facebook.buck.rules.FakeBuildRuleParamsBuilder; import com.facebook.buck.rules.FakeSourcePath; import com.facebook.buck.rules.SourcePathResolver; import com.facebook.buck.rules.SourcePathRuleFinder; import com.facebook.buck.rules.TargetGraph; import com.google.common.base.Preconditions; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Iterables; import java.io.File; import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.Collection; import java.util.Optional; import org.junit.Test; import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; /** High level tests for Precompiled header feature. */ @SuppressWarnings("PMD.TestClassWithoutTestCases") @RunWith(Enclosed.class) public class PrecompiledHeaderFeatureTest { /** Tests that PCH is only used when a preprocessor is declared to use PCH. */ @RunWith(Parameterized.class) public static class OnlyPrecompilePrefixHeaderIfToolchainIsSupported { @Parameterized.Parameter(0) public CxxToolProvider.Type toolType; @Parameterized.Parameter(1) public boolean pchEnabled; @Parameterized.Parameter(2) public boolean expectUsingPch; private CxxPlatform getPlatform() { return buildPlatform(toolType, pchEnabled); } @Parameterized.Parameters(name = "{1}") public static Collection<Object[]> data() { return Arrays.asList( new Object[][] { {CxxToolProvider.Type.CLANG, true, true}, {CxxToolProvider.Type.CLANG, false, false}, {CxxToolProvider.Type.GCC, true, true}, {CxxToolProvider.Type.GCC, false, false}, // TODO(steveo): add WINDOWS }); } @Test public void test() { final String headerFilename = "foo.h"; BuildRuleResolver resolver = new BuildRuleResolver(TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer()); CxxPreprocessAndCompile rule = preconfiguredSourceRuleFactoryBuilder(resolver) .setCxxPlatform(getPlatform()) .setCxxBuckConfig(buildConfig(pchEnabled)) .setPrefixHeader(new FakeSourcePath(headerFilename)) .setPrecompiledHeader(Optional.empty()) .build() .createPreprocessAndCompileBuildRule( "foo.c", preconfiguredCxxSourceBuilder().build()); boolean hasPchFlag = commandLineContainsPchFlag( new SourcePathResolver(new SourcePathRuleFinder(resolver)), rule, toolType, headerFilename); boolean hasPrefixFlag = commandLineContainsPrefixFlag( new SourcePathResolver(new SourcePathRuleFinder(resolver)), rule, toolType, headerFilename); assertNotEquals( "should use either prefix header flag, or precompiled header flag, but never both:" + " toolType:" + toolType + " pchEnabled:" + pchEnabled + ";" + " hasPrefixFlag:" + hasPrefixFlag + " hasPchFlag:" + hasPchFlag, hasPrefixFlag, hasPchFlag); assertEquals( "should precompile prefix header IFF supported and enabled:" + " toolType:" + toolType + " pchEnabled:" + pchEnabled + ";" + " expect:" + expectUsingPch, expectUsingPch, hasPchFlag); } } public static class TestSupportConditions { @Test public void rejectPchParameterIfSourceTypeDoesntSupportPch() { BuildRuleResolver resolver = new BuildRuleResolver(TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer()); CxxPlatform platform = PLATFORM_SUPPORTING_PCH.withCompilerDebugPathSanitizer( new MungingDebugPathSanitizer( 250, File.separatorChar, Paths.get("."), ImmutableBiMap.of())); CxxBuckConfig config = buildConfig(/* pchEnabled */ true); CxxSourceRuleFactory factory = preconfiguredSourceRuleFactoryBuilder(resolver) .setCxxPlatform(platform) .setPrefixHeader(new FakeSourcePath(("foo.pch"))) .setCxxBuckConfig(config) .build(); for (AbstractCxxSource.Type sourceType : AbstractCxxSource.Type.values()) { if (!sourceType.isPreprocessable()) { // Need a preprocessor object if we want to test for PCH'ability. continue; } switch (sourceType) { case ASM_WITH_CPP: case ASM: case CUDA: // The default platform we're testing with doesn't include preprocessors for these. continue; //$CASES-OMITTED$ default: Preprocessor preprocessor = CxxSourceTypes.getPreprocessor(platform, sourceType).resolve(resolver); assertEquals( sourceType.getPrecompiledHeaderLanguage().isPresent(), factory.canUsePrecompiledHeaders(config, preprocessor, sourceType)); } } } } public static class PrecompiledHeaderBuildTargetGeneration { @Test public void buildTargetShouldDeriveFromSanitizedFlags() { class TestData { public CxxPrecompiledHeader generate(Path from) { BuildRuleResolver resolver = new BuildRuleResolver( TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer()); CxxSourceRuleFactory factory = preconfiguredSourceRuleFactoryBuilder(resolver) .setCxxPlatform( PLATFORM_SUPPORTING_PCH.withCompilerDebugPathSanitizer( new MungingDebugPathSanitizer( 250, File.separatorChar, Paths.get("."), ImmutableBiMap.of(from, Paths.get("melon"))))) .setPrefixHeader(new FakeSourcePath(("foo.pch"))) .setCxxBuckConfig(buildConfig(/* pchEnabled */ true)) .build(); BuildRule rule = factory.createPreprocessAndCompileBuildRule( "foo.c", preconfiguredCxxSourceBuilder().addFlags("-I", from.toString()).build()); return FluentIterable.from(rule.getBuildDeps()) .filter(CxxPrecompiledHeader.class) .first() .get(); } } TestData testData = new TestData(); Path root = Preconditions.checkNotNull( Iterables.getFirst( FileSystems.getDefault().getRootDirectories(), Paths.get(File.separator))); CxxPrecompiledHeader firstRule = testData.generate(root.resolve("first")); CxxPrecompiledHeader secondRule = testData.generate(root.resolve("second")); assertEquals( "Build target flavor generator should normalize away the absolute paths", firstRule.getBuildTarget(), secondRule.getBuildTarget()); } @Test public void buildTargetShouldVaryWithCompilerFlags() { class TestData { public CxxPrecompiledHeader generate(Iterable<String> flags) { BuildRuleResolver resolver = new BuildRuleResolver( TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer()); CxxSourceRuleFactory factory = preconfiguredSourceRuleFactoryBuilder(resolver) .setCxxPlatform(PLATFORM_SUPPORTING_PCH) .setCxxBuckConfig(buildConfig(/* pchEnabled */ true)) .putAllCompilerFlags(CxxSource.Type.C_CPP_OUTPUT, flags) .setPrefixHeader(new FakeSourcePath(("foo.h"))) .build(); BuildRule rule = factory.createPreprocessAndCompileBuildRule( "foo.c", preconfiguredCxxSourceBuilder().build()); return FluentIterable.from(rule.getBuildDeps()) .filter(CxxPrecompiledHeader.class) .first() .get(); } } TestData testData = new TestData(); CxxPrecompiledHeader firstRule = testData.generate(ImmutableList.of("-target=x86_64-apple-darwin-macho")); CxxPrecompiledHeader secondRule = testData.generate(ImmutableList.of("-target=armv7-apple-watchos-macho")); assertNotEquals( "Build target flavor generator should account for compiler flags", firstRule.getBuildTarget(), secondRule.getBuildTarget()); } @Test public void buildTargetShouldVaryWithPreprocessorFlags() { class TestData { public CxxPrecompiledHeader generate(String flags) { BuildRuleResolver resolver = new BuildRuleResolver( TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer()); CxxSourceRuleFactory factory = preconfiguredSourceRuleFactoryBuilder(resolver) .setCxxPlatform(PLATFORM_SUPPORTING_PCH) .setCxxBuckConfig(buildConfig(/* pchEnabled */ true)) .setCxxPreprocessorInput( ImmutableList.of( CxxPreprocessorInput.builder() .setPreprocessorFlags(ImmutableMultimap.of(CxxSource.Type.C, flags)) .build())) .setPrefixHeader(new FakeSourcePath(("foo.h"))) .build(); BuildRule rule = factory.createPreprocessAndCompileBuildRule( "foo.c", preconfiguredCxxSourceBuilder().build()); return FluentIterable.from(rule.getBuildDeps()) .filter(CxxPrecompiledHeader.class) .first() .get(); } } TestData testData = new TestData(); CxxPrecompiledHeader firstRule = testData.generate("-DNDEBUG"); CxxPrecompiledHeader secondRule = testData.generate("-UNDEBUG"); assertNotEquals( "Build target flavor generator should account for preprocessor flags", firstRule.getBuildTarget(), secondRule.getBuildTarget()); } } // Helpers and defaults /** * Checks if the command line generated for the build rule contains the pch-inclusion directive. * * <p>This serves as an indicator that the file is being compiled with PCH enabled. */ private static boolean commandLineContainsPchFlag( SourcePathResolver resolver, CxxPreprocessAndCompile rule, CxxToolProvider.Type toolType, String headerFilename) { ImmutableList<String> flags = rule.makeMainStep(resolver, Paths.get("/tmp/unused_scratch_dir"), false).getCommand(); switch (toolType) { case CLANG: // Clang uses "-include-pch somefilename.h.gch" for (int i = 0; i + 1 < flags.size(); i++) { if (flags.get(i).equals("-include-pch") && flags.get(i + 1).endsWith(".h.gch")) { return true; } } break; case GCC: // For GCC we'd use: "-include sometargetname#someflavor-cxx-hexdigitsofhash.h", // i.e., it's the "-include" flag like in the prefix header case, but auto-gen-filename. for (int i = 0; i + 1 < flags.size(); i++) { if (flags.get(i).equals("-include") && !flags.get(i + 1).endsWith("/" + headerFilename)) { return true; } } break; case WINDOWS: // TODO(steveo): windows support in the near future. // (This case is not hit; parameters at top of this test class don't include WINDOWS.) throw new IllegalStateException(); case DEFAULT: // default not used in test. throw new IllegalStateException(); } return false; } private static boolean commandLineContainsPrefixFlag( SourcePathResolver resolver, CxxPreprocessAndCompile rule, CxxToolProvider.Type toolType, String headerFilename) { ImmutableList<String> flags = rule.makeMainStep(resolver, Paths.get("/tmp/unused_scratch_dir"), false).getCommand(); switch (toolType) { case CLANG: case GCC: // Clang and GCC use "-include somefilename.h". // GCC uses "-include" for precompiled headers as well, but in the GCC PCH case, we'll // pass a PCH filename that's auto-generated from a target name + flavors + hash chars // and other weird stuff. Here we'll be expecting the original header filename. for (int i = 0; i + 1 < flags.size(); i++) { if (flags.get(i).equals("-include") && flags.get(i + 1).endsWith("/" + headerFilename)) { return true; } } break; case WINDOWS: // TODO(steveo): windows support in the near future. // (This case is not hit; parameters at top of this test class don't include WINDOWS.) throw new IllegalStateException(); case DEFAULT: // default not used in test. throw new IllegalStateException(); } return false; } /** * Configures a CxxSourceRuleFactory.Builder with some sane defaults for PCH tests. Note: doesn't * call "setPrefixHeader", which actually sets the PCH parameters; caller needs to do that in * their various tests. */ private static CxxSourceRuleFactory.Builder preconfiguredSourceRuleFactoryBuilder( String targetPath, BuildRuleResolver ruleResolver) { SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(ruleResolver); SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder); BuildTarget target = BuildTargetFactory.newInstance(targetPath); BuildRuleParams params = new FakeBuildRuleParamsBuilder(target).build(); return CxxSourceRuleFactory.builder() .setParams(params) .setResolver(ruleResolver) .setPathResolver(pathResolver) .setRuleFinder(ruleFinder) .setPicType(AbstractCxxSourceRuleFactory.PicType.PDC); } private static CxxSourceRuleFactory.Builder preconfiguredSourceRuleFactoryBuilder( BuildRuleResolver resolver) { return preconfiguredSourceRuleFactoryBuilder("//foo:bar", resolver); } /** Configures a CxxSource.Builder representing a C source file. */ private static CxxSource.Builder preconfiguredCxxSourceBuilder() { return CxxSource.builder().setType(CxxSource.Type.C).setPath(new FakeSourcePath("foo.c")); } private static CxxBuckConfig buildConfig(boolean pchEnabled) { return new CxxBuckConfig( FakeBuckConfig.builder().setSections("[cxx]", "pch_enabled=" + pchEnabled).build()); } /** * Build a CxxPlatform for given preprocessor type. * * @return CxxPlatform containing a config which enables pch, and a preprocessor which may or may * not support PCH. */ private static CxxPlatform buildPlatform(CxxToolProvider.Type type, boolean pchEnabled) { return CxxPlatformUtils.build(buildConfig(pchEnabled)) .withCpp(new PreprocessorProvider(Paths.get("/usr/bin/foopp"), Optional.of(type))); } private static final CxxPlatform PLATFORM_SUPPORTING_PCH = buildPlatform(CxxToolProvider.Type.CLANG, true); }