/* * 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.file; import static com.facebook.buck.util.environment.Platform.WINDOWS; import static java.nio.charset.StandardCharsets.UTF_8; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.junit.Assume.assumeThat; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.model.BuildTargetFactory; import com.facebook.buck.rules.BuildRuleParams; import com.facebook.buck.rules.BuildRuleResolver; import com.facebook.buck.rules.BuildableContext; import com.facebook.buck.rules.DefaultTargetNodeToBuildRuleTransformer; import com.facebook.buck.rules.FakeBuildContext; import com.facebook.buck.rules.FakeBuildRuleParamsBuilder; import com.facebook.buck.rules.FakeBuildableContext; import com.facebook.buck.rules.SourcePathResolver; import com.facebook.buck.rules.SourcePathRuleFinder; import com.facebook.buck.rules.TargetGraph; import com.facebook.buck.step.ExecutionContext; import com.facebook.buck.step.Step; import com.facebook.buck.step.TestExecutionContext; import com.facebook.buck.util.environment.Platform; import com.google.common.collect.ImmutableList; import com.google.common.hash.HashCode; import com.google.common.hash.Hashing; import java.io.IOException; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import javax.annotation.Nullable; import org.easymock.EasyMock; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; public class RemoteFileTest { @Rule public TemporaryFolder tmp = new TemporaryFolder(); @Test public void ensureOutputIsAddedToBuildableContextSoItIsCached() throws Exception { Downloader downloader = new ExplodingDownloader(); BuildTarget target = BuildTargetFactory.newInstance("//cheese:cake"); BuildRuleResolver resolver = new BuildRuleResolver(TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer()); SourcePathResolver pathResolver = new SourcePathResolver(new SourcePathRuleFinder(resolver)); RemoteFile remoteFile = new RemoteFileBuilder(downloader, target) .setUrl("http://www.facebook.com/") .setSha1(Hashing.sha1().hashLong(42)) .build(resolver); BuildableContext buildableContext = EasyMock.createNiceMock(BuildableContext.class); buildableContext.recordArtifact( pathResolver.getRelativePath(remoteFile.getSourcePathToOutput())); EasyMock.replay(buildableContext); remoteFile.getBuildSteps( FakeBuildContext.withSourcePathResolver(pathResolver), buildableContext); EasyMock.verify(buildableContext); } @Test public void shouldSaveToFinalLocationAfterSha1IsVerified() throws Exception { String value = "I like cake"; HashCode hashCode = Hashing.sha1().hashBytes(value.getBytes(UTF_8)); Path output = runTheMagic(null, value, hashCode); assertThat(output, exists()); } @Test public void shouldNotSaveToFinalLocationUntilAfterSha1IsVerified() throws Exception { Path output = runTheMagic(null, "eat more cheese", Hashing.sha1().hashLong(42)); assertThat(output, not(exists())); } @Test public void shouldNotSaveFileToFinalLocationIfTheDownloadFails() throws Exception { String value = "I also like cake"; HashCode hashCode = Hashing.sha1().hashBytes(value.getBytes(UTF_8)); Path output = runTheMagic(new ExplodingDownloader(), value, hashCode); assertThat(output, not(exists())); } @Test public void shouldNotMakeDownloadedFileExecutableWhenTypeIsData() throws Exception { assumeThat(Platform.detect(), is(not(WINDOWS))); String value = "I like cake"; HashCode hashCode = Hashing.sha1().hashBytes(value.getBytes(UTF_8)); Path output = runTheMagic(null, value, hashCode, RemoteFile.Type.DATA); assertThat(output, exists()); assertThat(output, not(isExecutable())); } @Test public void shouldMakeDownloadedFileExecutableIfRequested() throws Exception { assumeThat(Platform.detect(), is(not(WINDOWS))); String value = "I like cake"; HashCode hashCode = Hashing.sha1().hashBytes(value.getBytes(UTF_8)); Path output = runTheMagic(null, value, hashCode, RemoteFile.Type.EXECUTABLE); assertThat(output, exists()); assertThat(output, isExecutable()); } @Test public void shouldUnzipExplodedArchive() throws Exception { // zip archive of a directory called hello, which contains hello.txt file with "hello\n" content final byte[] archiveContent = { 0x50, 0x4B, 0x03, 0x04, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xED, 0x5B, 0x26, 0x4A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x1C, 0x00, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x2F, 0x55, 0x54, 0x09, 0x00, 0x03, (byte) 0x8D, (byte) 0xF0, 0x6F, 0x58, (byte) 0x94, (byte) 0xF0, 0x6F, 0x58, 0x75, 0x78, 0x0B, 0x00, 0x01, 0x04, (byte) 0xAD, (byte) 0x9E, (byte) 0xC2, 0x5D, 0x04, 0x00, 0x00, 0x00, 0x00, 0x50, 0x4B, 0x03, 0x04, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x59, 0x26, 0x4A, 0x20, 0x30, 0x3A, 0x36, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x1C, 0x00, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x2F, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x2E, 0x74, 0x78, 0x74, 0x55, 0x54, 0x09, 0x00, 0x03, 0x3A, (byte) 0xEB, 0x6F, 0x58, (byte) 0x94, (byte) 0xF0, 0x6F, 0x58, 0x75, 0x78, 0x0B, 0x00, 0x01, 0x04, (byte) 0xAD, (byte) 0x9E, (byte) 0xC2, 0x5D, 0x04, 0x00, 0x00, 0x00, 0x00, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x0A, 0x50, 0x4B, 0x01, 0x02, 0x1E, 0x03, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xED, 0x5B, 0x26, 0x4A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, (byte) 0xED, 0x41, 0x00, 0x00, 0x00, 0x00, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x2F, 0x55, 0x54, 0x05, 0x00, 0x03, (byte) 0x8D, (byte) 0xF0, 0x6F, 0x58, 0x75, 0x78, 0x0B, 0x00, 0x01, 0x04, (byte) 0xAD, (byte) 0x9E, (byte) 0xC2, 0x5D, 0x04, 0x00, 0x00, 0x00, 0x00, 0x50, 0x4B, 0x01, 0x02, 0x1E, 0x03, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x59, 0x26, 0x4A, 0x20, 0x30, 0x3A, 0x36, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, (byte) 0xA4, (byte) 0x81, 0x40, 0x00, 0x00, 0x00, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x2F, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x2E, 0x74, 0x78, 0x74, 0x55, 0x54, 0x05, 0x00, 0x03, 0x3A, (byte) 0xEB, 0x6F, 0x58, 0x75, 0x78, 0x0B, 0x00, 0x01, 0x04, (byte) 0xAD, (byte) 0x9E, (byte) 0xC2, 0x5D, 0x04, 0x00, 0x00, 0x00, 0x00, 0x50, 0x4B, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, (byte) 0xA1, 0x00, 0x00, 0x00, (byte) 0x8F, 0x00, 0x00, 0x00, 0x00, 0x00, }; HashCode hashCode = Hashing.sha1().hashBytes(archiveContent); Path output = runTheMagic(archiveContent, hashCode, RemoteFile.Type.EXPLODED_ZIP); assertThat(output, exists()); Path filePath = output.resolve("hello").resolve("hello.txt"); assertThat(filePath, exists()); assertThat(filePath, hasContent("hello\n".getBytes(UTF_8))); } private static Matcher<Path> exists() { return new BaseMatcher<Path>() { @Override public boolean matches(Object o) { return Files.exists((Path) o); } @Override public void describeTo(Description description) { description.appendText("File must exist"); } }; } private static Matcher<Path> isExecutable() { return new BaseMatcher<Path>() { @Override public boolean matches(Object o) { return Files.isExecutable((Path) o); } @Override public void describeTo(Description description) { description.appendText("File must be executable"); } }; } private static Matcher<Path> hasContent(final byte[] content) { return new BaseMatcher<Path>() { @Override public boolean matches(Object o) { try { return Arrays.equals(Files.readAllBytes((Path) o), content); } catch (IOException e) { throw new RuntimeException(e); } } @Override public void describeTo(Description description) { description.appendText("File has unexpected content"); } }; } private Path runTheMagic( @Nullable Downloader downloader, String contentsOfFile, HashCode hashCode) throws Exception { return runTheMagic(downloader, contentsOfFile, hashCode, RemoteFile.Type.DATA); } private Path runTheMagic( @Nullable Downloader downloader, String contentsOfFile, HashCode hashCode, RemoteFile.Type type) throws Exception { return runTheMagic(downloader, contentsOfFile.getBytes(UTF_8), hashCode, type); } private Path runTheMagic(final byte[] contentsOfFile, HashCode hashCode, RemoteFile.Type type) throws Exception { Downloader downloader = (eventBus, uri, output) -> { Files.createDirectories(output.getParent()); Files.write(output, contentsOfFile); return true; }; return runTheMagic(downloader, contentsOfFile, hashCode, type); } private Path runTheMagic( @Nullable Downloader downloader, final byte[] contentsOfFile, HashCode hashCode, RemoteFile.Type type) throws Exception { if (downloader == null) { downloader = (eventBus, uri, output) -> { Files.createDirectories(output.getParent()); Files.write(output, contentsOfFile); return true; }; } ProjectFilesystem filesystem = new ProjectFilesystem(tmp.getRoot().toPath().toAbsolutePath()); BuildRuleParams params = new FakeBuildRuleParamsBuilder("//cake:walk").setProjectFilesystem(filesystem).build(); RemoteFile remoteFile = new RemoteFile( params, downloader, new URI("http://example.com"), hashCode, "output.txt", type); BuildRuleResolver resolver = new BuildRuleResolver(TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer()); resolver.addToIndex(remoteFile); SourcePathResolver pathResolver = new SourcePathResolver(new SourcePathRuleFinder(resolver)); ImmutableList<Step> buildSteps = remoteFile.getBuildSteps( FakeBuildContext.withSourcePathResolver(pathResolver), new FakeBuildableContext()); ExecutionContext context = TestExecutionContext.newInstance(); for (Step buildStep : buildSteps) { int result = buildStep.execute(context).getExitCode(); if (result != 0) { break; } } return pathResolver.getAbsolutePath(remoteFile.getSourcePathToOutput()); } }