/*
* Copyright 2013 Google Inc. 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.jenkins.plugins.dsl;
import static java.util.concurrent.TimeUnit.SECONDS;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.concurrent.Future;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.jvnet.hudson.test.JenkinsRule;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static com.google.common.io.ByteStreams.copy;
import com.google.common.collect.ImmutableList;
import com.google.common.io.CharStreams;
import com.google.common.io.Files;
import com.google.common.reflect.TypeToken;
import com.google.common.util.concurrent.Uninterruptibles;
import com.google.jenkins.plugins.dsl.restrict.NoRestriction;
import com.google.jenkins.plugins.dsl.restrict.PluginBlacklist;
import com.google.jenkins.plugins.storage.GoogleCloudStorageUploader;
import com.google.jenkins.plugins.storage.StdoutUpload;
import hudson.PluginManager;
import hudson.PluginWrapper;
import hudson.model.AbstractProject;
import hudson.model.Cause;
import hudson.model.FreeStyleProject;
import hudson.model.ParametersAction;
import hudson.model.Queue;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.StringParameterValue;
import hudson.model.TopLevelItem;
import hudson.model.View;
import hudson.scm.NullSCM;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
/**
* Tests for {@link YamlProject}.
* @param <T>
*/
public class YamlProjectTest<T extends AbstractProject & TopLevelItem> {
@Rule
public JenkinsRule jenkins = new JenkinsRule();
@Rule
public TemporaryFolder folder = new TemporaryFolder();
@Rule
public Retry retry = new Retry(3);
private File yamlFile;
private File innerFile;
private YamlProject<T> underTest;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
TypeToken<YamlProject<T>> token =
new TypeToken<YamlProject<T>>() {};
underTest = Jenkins.getInstance().createProject(
(Class<YamlProject<T>>) token.getRawType(), "underTest");
yamlFile = folder.newFile(".dsl.yaml");
innerFile = folder.newFile(".inner.yaml");
underTest.setYamlPath(yamlFile.getAbsolutePath());
underTest.setRestriction(new NoRestriction());
// The majority of tests should be what most users will see
toggleVerbosity(false);
}
private void toggleVerbosity(boolean verbose) throws Exception {
YamlProject.DescriptorImpl descriptor =
underTest.getDescriptor();
JSONObject json = new JSONObject();
json.put("verboseLogging", verbose);
JSONObject form = new JSONObject();
form.put(descriptor.getDisplayName(), json);
assertTrue(descriptor.configure(null, form));
}
@Test
public void testSimple() throws Exception {
writeResourceToFile("foo.yaml");
assertEquals(0, underTest.getItems().size());
YamlBuild build = underTest.scheduleBuild2(0).get();
dumpLog(build);
assertEquals(Result.SUCCESS, build.getResult());
assertThat(CharStreams.toString(new InputStreamReader(
build.getLogInputStream())), allOf(
containsString("Hello World"),
// Validation that verbose logging is disabled
not(containsString("!freestyle")),
not(containsString("!shell")),
not(containsString("hudson.model.FreeStyleProject")),
not(containsString("hudson.tasks.Shell"))));
assertEquals(1, underTest.getItems().size());
YamlHistoryAction action = YamlHistoryAction.of(build);
assertNotNull(action.getProject(underTest));
assertEquals(1, action.getBuild(underTest).getNumber());
}
@Test
public void testSimpleWithVerboseLogging() throws Exception {
writeResourceToFile("foo.yaml");
toggleVerbosity(true);
assertEquals(0, underTest.getItems().size());
YamlBuild build = underTest.scheduleBuild2(0).get();
dumpLog(build);
assertEquals(Result.SUCCESS, build.getResult());
assertThat(CharStreams.toString(new InputStreamReader(
build.getLogInputStream())), allOf(
containsString("Hello World"),
// Validation that verbose logging is enabled
containsString("!freestyle"),
containsString("!shell"),
containsString("hudson.model.FreeStyleProject"),
containsString("hudson.tasks.Shell")));
assertEquals(1, underTest.getItems().size());
YamlHistoryAction action = YamlHistoryAction.of(build);
assertNotNull(action.getProject(underTest));
assertEquals(1, action.getBuild(underTest).getNumber());
}
@Test
public void testSimpleWithDefaultParameter() throws Exception {
writeResourceToFile("param.yaml");
assertEquals(0, underTest.getItems().size());
YamlBuild build = underTest.scheduleBuild2(0,
new Cause.LegacyCodeCause()).get();
dumpLog(build);
assertEquals(Result.SUCCESS, build.getResult());
assertThat(CharStreams.toString(new InputStreamReader(
build.getLogInputStream())),
// Check that we wrote the default value.
containsString("bar"));
}
@Test
public void testSimpleWithParameter() throws Exception {
writeResourceToFile("param.yaml");
assertEquals(0, underTest.getItems().size());
ParametersAction parameters = new ParametersAction(
new StringParameterValue("foo", "baz"));
YamlBuild build = underTest.scheduleBuild2(0,
new Cause.LegacyCodeCause(), parameters).get();
dumpLog(build);
assertEquals(Result.SUCCESS, build.getResult());
assertThat(CharStreams.toString(new InputStreamReader(
build.getLogInputStream())),
// Check that we wrote the passed in parameter instead
// of the default value.
containsString("baz"));
}
@Test
public void testSimpleCancellation() throws Exception {
writeResourceToFile("label.yaml");
// Make it so that any scheduled inner builds cannot make
// progress, so we may cancel it to test that the Stage
// properly terminates
jenkins.jenkins.setNumExecutors(0);
jenkins.jenkins.setLabelString("NOT_THE_DROIDS_YOU_ARE_LOOKING_FOR");
Future<YamlBuild<T>> outerBuild = underTest.scheduleBuild2(0);
boolean cancelled = false;
// Find and cancel the scheduled child job.
for (int i = 0; i < 50 && !cancelled; ++i) {
boolean empty = true;
// Walk the queue looking for an item that represents our child execution.
for (final Queue.Item item : Queue.getInstance().getItems()) {
empty = false;
if (item.task == underTest) {
Uninterruptibles.sleepUninterruptibly(1, SECONDS);
} else {
// Cancel the queued item.
assertTrue(item.isStuck());
assertTrue(Queue.getInstance().cancel(item));
// NOTE: We are not seeing the future cancelled as part of the above
// call, so we fall back on checking that the item with the same 'id'
// is now a LeftItem (as in: "left" the queue), which shows as
// isCancelled().
// assertTrue(item.getFuture().isCancelled());
Queue.Item newItem = Queue.getInstance().getItem(item.getId());
assertNotNull(newItem);
assertThat(newItem, instanceOf(Queue.LeftItem.class));
assertTrue(((Queue.LeftItem) newItem).isCancelled());
cancelled = true;
break;
}
}
// If we see the queue in an empty state, wait for entries to appear.
if (empty) {
Uninterruptibles.sleepUninterruptibly(1, SECONDS);
}
}
assertTrue(cancelled);
YamlBuild build = outerBuild.get(3, SECONDS);
dumpLog(build);
assertEquals(Result.ABORTED, build.getResult());
}
@Test
public void testDisallowedNullScm() throws Exception {
YamlProject.DescriptorImpl descriptor =
underTest.getDescriptor();
assertFalse(descriptor.isApplicable(
Jenkins.getInstance().getDescriptor(NullSCM.class)));
}
@Test
public void testFailure() throws Exception {
writeResourceToFile("fail.yaml");
assertEquals(0, underTest.getItems().size());
YamlBuild build = underTest.scheduleBuild2(0).get();
dumpLog(build);
// Verify that if the inner build fails, the outer build
// reports as failure.
assertEquals(Result.FAILURE, build.getResult());
}
@Test
public void testMissingFile() throws Exception {
// Intentionally not writing DSL file.
underTest.setYamlPath(".missing.yaml");
assertEquals(0, underTest.getItems().size());
YamlBuild build = underTest.scheduleBuild2(0).get();
dumpLog(build);
// Verify that if the inner build fails, the outer build
// reports as failure.
assertEquals(Result.FAILURE, build.getResult());
assertThat(CharStreams.toString(new InputStreamReader(
build.getLogInputStream())),
containsString("No .missing.yaml file in workspace"));
}
@Test
public void testActionPromotion() throws Exception {
writeResourceToFile("junit.yaml");
assertEquals(0, underTest.getItems().size());
YamlBuild build = underTest.scheduleBuild2(0).get();
dumpLog(build);
// Verify that if the inner build fails, the outer build
// reports as failure.
assertEquals(Result.SUCCESS, build.getResult());
assertNotNull(build.getAction(hudson.tasks.junit.TestResultAction.class));
}
@Test
public void testNoChange() throws Exception {
writeResourceToFile("foo.yaml");
assertEquals(0, underTest.getItems().size());
YamlBuild build = underTest.scheduleBuild2(0).get();
dumpLog(build);
assertEquals(Result.SUCCESS, build.getResult());
assertThat(CharStreams.toString(new InputStreamReader(
build.getLogInputStream())), containsString("Hello World"));
assertEquals(1, underTest.getItems().size());
YamlHistoryAction firstBuildAction = YamlHistoryAction.of(build);
assertNotNull(firstBuildAction.getProject(underTest));
assertEquals(1, firstBuildAction.getBuild(underTest).getNumber());
// Verify that a second build with no change doesn't instantiate
// a second sub-project
build = underTest.scheduleBuild2(0).get();
dumpLog(build);
assertEquals(Result.SUCCESS, build.getResult());
assertThat(CharStreams.toString(new InputStreamReader(
build.getLogInputStream())), containsString("Hello World"));
assertEquals(1, underTest.getItems().size());
YamlHistoryAction secondBuildAction = YamlHistoryAction.of(build);
assertSame(firstBuildAction.getProject(underTest),
secondBuildAction.getProject(underTest));
assertEquals(2, secondBuildAction.getBuild(underTest).getNumber());
}
@Test
public void testSimpleChange() throws Exception {
writeResourceToFile("foo.yaml");
assertEquals(0, underTest.getItems().size());
YamlBuild build = underTest.scheduleBuild2(0).get();
dumpLog(build);
assertEquals(Result.SUCCESS, build.getResult());
assertThat(CharStreams.toString(new InputStreamReader(
build.getLogInputStream())), containsString("Hello World"));
assertEquals(1, underTest.getItems().size());
YamlHistoryAction firstBuildAction = YamlHistoryAction.of(build);
assertNotNull(firstBuildAction.getProject(underTest));
assertEquals(1, firstBuildAction.getBuild(underTest).getNumber());
// Verify that a simple change results in a second project
// being created.
writeResourceToFile("bar.yaml");
build = underTest.scheduleBuild2(0).get();
dumpLog(build);
assertEquals(Result.SUCCESS, build.getResult());
assertThat(CharStreams.toString(
new InputStreamReader(build.getLogInputStream())),
allOf(not(containsString("Hello World")),
containsString("Hola Mundo")));
assertEquals(2, underTest.getItems().size());
YamlHistoryAction secondBuildAction = YamlHistoryAction.of(build);
assertNotSame(firstBuildAction.getProject(underTest),
secondBuildAction.getProject(underTest));
assertEquals(1, secondBuildAction.getBuild(underTest).getNumber());
// Verify that the original project's workspace has been deleted
assertFalse(firstBuildAction.getBuild(underTest).getWorkspace().exists());
}
@Test
public void testProjectTypeChange() throws Exception {
writeResourceToFile("foo.yaml");
assertEquals(0, underTest.getItems().size());
YamlBuild build = underTest.scheduleBuild2(0).get();
dumpLog(build);
assertEquals(Result.SUCCESS, build.getResult());
assertThat(CharStreams.toString(new InputStreamReader(
build.getLogInputStream())), containsString("Hello World"));
assertEquals(1, underTest.getItems().size());
YamlHistoryAction firstBuildAction = YamlHistoryAction.of(build);
assertNotNull(firstBuildAction.getProject(underTest));
assertEquals(1, firstBuildAction.getBuild(underTest).getNumber());
// Verify that we can change project types as easily as any other
// change to the DSL.
writeResourceToFile("matrix.yaml");
build = underTest.scheduleBuild2(0).get();
dumpLog(build);
assertEquals(Result.SUCCESS, build.getResult());
assertThat(CharStreams.toString(
new InputStreamReader(build.getLogInputStream())),
allOf(not(containsString("Hello World")),
// As a matrix build, this output should be inside of the
// output of the sub-job.
not(containsString("Hola Mundo"))));
assertEquals(2, underTest.getItems().size());
YamlHistoryAction secondBuildAction = YamlHistoryAction.of(build);
assertNotSame(firstBuildAction.getProject(underTest),
secondBuildAction.getProject(underTest));
assertEquals(1, secondBuildAction.getBuild(underTest).getNumber());
}
@Mock
private PluginWrapper plugin;
@Mock
private PluginManager manager;
/**
* Special version of PluginBlacklist that hosts our manager mock in a
* transient field, so that it can be serialized (output only)
*/
private static class CustomBlacklist extends PluginBlacklist {
public CustomBlacklist(List<String> plugins, PluginManager manager) {
super(plugins);
this.manager = manager;
}
@Override
public PluginManager getPluginManager() {
return manager;
}
// We want to be able to use a mock, but need this to be saveable.
private transient PluginManager manager;
}
@Test
public void testRestrictionFailure_DescribableList() throws Exception {
writeResourceToFile("bad.yaml");
underTest.setRestriction(new CustomBlacklist(
ImmutableList.of("google-storage-plugin"), manager));
when(manager.whichPlugin(GoogleCloudStorageUploader.class))
.thenReturn(plugin);
when(plugin.getShortName()).thenReturn("google-storage-plugin");
assertEquals(0, underTest.getItems().size());
YamlBuild build = underTest.scheduleBuild2(0).get();
dumpLog(build);
assertEquals(Result.FAILURE, build.getResult());
assertThat(CharStreams.toString(new InputStreamReader(
build.getLogInputStream())), allOf(
containsString("RestrictedTypeException"),
containsString(GoogleCloudStorageUploader.class.getName())));
}
@Test
public void testRestrictionFailure_bindJSON() throws Exception {
writeResourceToFile("bad.yaml");
underTest.setRestriction(new CustomBlacklist(
ImmutableList.of("google-storage-plugin"), manager));
// This time, pretend GCSUploader is built-in,
// but when we see StdoutUpload, then complain.
when(manager.whichPlugin(StdoutUpload.class)).thenReturn(plugin);
when(plugin.getShortName()).thenReturn("google-storage-plugin");
assertEquals(0, underTest.getItems().size());
YamlBuild build = underTest.scheduleBuild2(0).get();
dumpLog(build);
assertEquals(Result.FAILURE, build.getResult());
assertThat(CharStreams.toString(new InputStreamReader(
build.getLogInputStream())), allOf(
containsString("RestrictedTypeException"),
containsString(StdoutUpload.class.getName())));
}
@Test
public void testRestrictionFailure_topLevel() throws Exception {
writeResourceToFile("bad.yaml");
underTest.setRestriction(new CustomBlacklist(
ImmutableList.of("google-storage-plugin"), manager));
// This time, pretend FreeStyleProject is from our blacklisted plugin
when(manager.whichPlugin(FreeStyleProject.class)).thenReturn(plugin);
when(plugin.getShortName()).thenReturn("google-storage-plugin");
assertEquals(0, underTest.getItems().size());
YamlBuild build = underTest.scheduleBuild2(0).get();
dumpLog(build);
assertEquals(Result.FAILURE, build.getResult());
assertThat(CharStreams.toString(new InputStreamReader(
build.getLogInputStream())), allOf(
containsString("RestrictedTypeException"),
containsString(FreeStyleProject.class.getName())));
}
@Test
public void testDiagnosticFailure_notLoaded() throws Exception {
writeResourceToFile("typo.yaml");
YamlBuild build = underTest.scheduleBuild2(0).get();
dumpLog(build);
assertEquals(Result.FAILURE, build.getResult());
assertThat(CharStreams.toString(new InputStreamReader(
build.getLogInputStream())), allOf(
containsString("BadTypeException"),
containsString("Shellz")));
}
@Test
public void testDiagnosticFailure_outOfPlace() throws Exception {
writeResourceToFile("out-of-place.yaml");
YamlBuild build = underTest.scheduleBuild2(0).get();
dumpLog(build);
assertEquals(Result.FAILURE, build.getResult());
assertThat(CharStreams.toString(new InputStreamReader(
build.getLogInputStream())), allOf(
containsString("BadTypeException"),
containsString("Shell")));
}
@Test
public void testDiagnosticFailure_NestedDescribable() throws Exception {
writeResourceToFile("inner-typo.yaml");
YamlBuild build = underTest.scheduleBuild2(0).get();
dumpLog(build);
assertEquals(Result.FAILURE, build.getResult());
assertThat(CharStreams.toString(new InputStreamReader(
build.getLogInputStream())), allOf(
containsString("BadTypeException"),
containsString("StdoutUploadz")));
}
/**
* Make sure that a user can't just check in a DSL that delegates to a child
* DSL job in order to circumvent the parent DSL job's restrictions.
*/
@Test
public void testRestrictionFailure_trampolineJob() throws Exception {
String body = readResource("trampoline.yaml");
body = body.replaceAll(".inner.yaml", innerFile.toString());
writeStringToFile(body, yamlFile);
writeResourceToFile("bad.yaml", innerFile);
underTest.setRestriction(new CustomBlacklist(
ImmutableList.of("google-storage-plugin"), manager));
when(manager.whichPlugin(GoogleCloudStorageUploader.class))
.thenReturn(plugin);
when(plugin.getShortName()).thenReturn("google-storage-plugin");
assertEquals(0, underTest.getItems().size());
YamlBuild build = underTest.scheduleBuild2(0).get();
dumpLog(build);
assertEquals(Result.FAILURE, build.getResult());
assertThat(CharStreams.toString(new InputStreamReader(
build.getLogInputStream())), allOf(
containsString("RestrictedTypeException"),
containsString(GoogleCloudStorageUploader.class.getName())));
}
@Mock
private View mockView;
@Test(expected = UnsupportedOperationException.class)
public void testUnsupportedOperation_onViewRenamed() throws Exception {
underTest.onViewRenamed(mockView, "old", "new");
}
@Test(expected = UnsupportedOperationException.class)
public void testUnsupportedOperation_deleteView() throws Exception {
underTest.deleteView(mockView);
}
@Test
public void testUnsupportedOperation_canDelete() throws Exception {
assertFalse(underTest.canDelete(mockView));
}
@Mock
private T mockItem;
@Test(expected = UnsupportedOperationException.class)
public void testUnsupportedOperation_onDeleted() throws Exception {
underTest.onDeleted(mockItem);
}
@Test(expected = UnsupportedOperationException.class)
public void testUnsupportedOperation_onRenamed() throws Exception {
underTest.onRenamed(mockItem, "old", "new");
}
@Test
public void testViewProperties() throws Exception {
writeResourceToFile("foo.yaml");
JobHistoryView view = underTest.getJobHistoryView();
assertEquals(0, underTest.getItems().size());
assertEquals(ImmutableList.copyOf(underTest.getItems()), view.getItems());
// Check that there is no "last project"
assertNull(underTest.getLastProject());
YamlBuild build = underTest.scheduleBuild2(0).get();
dumpLog(build);
assertEquals(Result.SUCCESS, build.getResult());
assertEquals(ImmutableList.copyOf(underTest.getItems()), view.getItems());
// Check that the parent of the last build is our "last project"
YamlHistoryAction action = YamlHistoryAction.of(build);
assertEquals(action.getProject(underTest), underTest.getLastProject());
// Verify that a simple change results in a second project
// being created.
writeResourceToFile("bar.yaml");
build = underTest.scheduleBuild2(0).get();
dumpLog(build);
assertEquals(Result.SUCCESS, build.getResult());
assertEquals(ImmutableList.copyOf(underTest.getItems()), view.getItems());
// Check that the parent of the last build is our "last project"
action = YamlHistoryAction.of(build);
assertEquals(action.getProject(underTest), underTest.getLastProject());
}
private void writeResourceToFile(String resourceName) throws IOException {
writeResourceToFile(resourceName, yamlFile);
}
private void writeResourceToFile(String resourceName, File file)
throws IOException {
final InputStream inputStream =
getClass().getClassLoader().getResourceAsStream(
"com/google/jenkins/plugins/dsl/" + resourceName);
copy(inputStream, Files.newOutputStreamSupplier(file));
}
private String readResource(String resourceName)
throws IOException {
final InputStream inputStream =
getClass().getClassLoader().getResourceAsStream(
"com/google/jenkins/plugins/dsl/" + resourceName);
ByteArrayOutputStream output = new ByteArrayOutputStream();
copy(inputStream, output);
return output.toString();
}
private void writeStringToFile(String body, File file)
throws IOException {
final InputStream inputStream = new ByteArrayInputStream(body.getBytes());
copy(inputStream, Files.newOutputStreamSupplier(file));
}
private void dumpLog(Run run) throws IOException {
BufferedReader reader = new BufferedReader(run.getLogReader());
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}