diff --git a/pom.xml b/pom.xml index 36f91c0..9b14c59 100644 --- a/pom.xml +++ b/pom.xml @@ -80,12 +80,26 @@ nl.knaw.dans dans-dataverse-client-lib - org.apache.commons commons-csv + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + diff --git a/src/main/java/nl/knaw/dans/dvcli/action/BatchProcessor.java b/src/main/java/nl/knaw/dans/dvcli/action/BatchProcessor.java index 6068f09..ff18506 100644 --- a/src/main/java/nl/knaw/dans/dvcli/action/BatchProcessor.java +++ b/src/main/java/nl/knaw/dans/dvcli/action/BatchProcessor.java @@ -16,6 +16,7 @@ package nl.knaw.dans.dvcli.action; import lombok.Builder; +import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import java.util.List; @@ -34,17 +35,20 @@ public class BatchProcessor { /** * The labeled items to process. */ + @NonNull private final List> labeledItems; /** * The action to apply to each item. */ + @NonNull private final ThrowingFunction action; /** * The report to which the results of the actions are reported. */ - private final Report report; + @Builder.Default + private final Report report = new ConsoleReport<>(); /** * The delay in milliseconds between processing items. A delay of 0 or less means no delay. @@ -60,7 +64,7 @@ public void process() { log.info("Processing item {} of {}", ++i, labeledItems.size()); callAction(labeledItem.getFirst(), labeledItem.getSecond()); } - log.info("Finished batch processing"); + log.info("Finished batch processing of {} items", labeledItems.size()); } private void callAction(String label, I item) { diff --git a/src/main/java/nl/knaw/dans/dvcli/action/SingleIdOrIdsFile.java b/src/main/java/nl/knaw/dans/dvcli/action/SingleIdOrIdsFile.java index 97f6ac6..6b1b9d7 100644 --- a/src/main/java/nl/knaw/dans/dvcli/action/SingleIdOrIdsFile.java +++ b/src/main/java/nl/knaw/dans/dvcli/action/SingleIdOrIdsFile.java @@ -38,7 +38,7 @@ public Stream getPids() throws IOException { return Stream.of(defaultId); } - Stream lines = null; + Stream lines; if ("-".equals(singleIdOrIdFile)) { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); @@ -47,6 +47,9 @@ public Stream getPids() throws IOException { else { var pidFile = Paths.get(singleIdOrIdFile); if (Files.exists(pidFile)) { + if (!Files.isRegularFile(pidFile)) { + throw new IOException(singleIdOrIdFile + " is not a regular file"); + } lines = Files.lines(pidFile) .flatMap(line -> Arrays.stream(line.trim().split("\\s+"))); } diff --git a/src/main/java/nl/knaw/dans/dvcli/command/AbstractSubcommandContainer.java b/src/main/java/nl/knaw/dans/dvcli/command/AbstractSubcommandContainer.java index a59c9c9..85cddb1 100644 --- a/src/main/java/nl/knaw/dans/dvcli/command/AbstractSubcommandContainer.java +++ b/src/main/java/nl/knaw/dans/dvcli/command/AbstractSubcommandContainer.java @@ -19,7 +19,9 @@ import nl.knaw.dans.dvcli.action.BatchProcessor; import nl.knaw.dans.dvcli.action.Pair; import nl.knaw.dans.dvcli.action.SingleIdOrIdsFile; +import nl.knaw.dans.dvcli.action.ThrowingFunction; import nl.knaw.dans.lib.dataverse.DataverseClient; +import nl.knaw.dans.lib.dataverse.DataverseException; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; @@ -28,7 +30,7 @@ public abstract class AbstractSubcommandContainer extends AbstractCmd { private static final long DEFAULT_DELAY = 1000; - + protected DataverseClient dataverseClient; public AbstractSubcommandContainer(@NonNull DataverseClient dataverseClient) { @@ -37,7 +39,7 @@ public AbstractSubcommandContainer(@NonNull DataverseClient dataverseClient) { @Parameters(index = "0", description = "The target(s) of the operation; this is either an ID a file with a with a list of IDs, or - if the subcommand supports it - a parameters file.", paramLabel = "targets", defaultValue = SingleIdOrIdsFile.DEFAULT_TARGET_PLACEHOLDER) - + protected String targets; @Option(names = { "-d", "--delay" }, description = "Delay in milliseconds between requests to the server (default: ${DEFAULT-VALUE}).", defaultValue = "" + DEFAULT_DELAY) @@ -48,15 +50,23 @@ protected BatchProcessor.BatchProcessorBuilder batchProcessorBuilder( .labeledItems(getItems()) .delay(delay); } - - protected

BatchProcessor.BatchProcessorBuilder paramsBatchProcessorBuilder() throws IOException { + + protected

BatchProcessor.BatchProcessorBuilder paramsBatchProcessorBuilder() { return BatchProcessor. builder() .delay(delay); } protected abstract List> getItems() throws IOException; - + @Override - public void doCall() { + public void doCall() throws IOException, DataverseException { + } + + public BatchProcessor batchProcessor(ThrowingFunction action) throws IOException { + return BatchProcessor. builder() + .labeledItems(getItems()) + .delay(delay) + .action(action) + .build(); } } diff --git a/src/test/java/nl/knaw/dans/dvcli/AbstractCapturingTest.java b/src/test/java/nl/knaw/dans/dvcli/AbstractCapturingTest.java new file mode 100644 index 0000000..ab1a63a --- /dev/null +++ b/src/test/java/nl/knaw/dans/dvcli/AbstractCapturingTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 DANS - Data Archiving and Networked Services (info@dans.knaw.nl) + * + * 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 nl.knaw.dans.dvcli; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.io.PrintStream; + +public abstract class AbstractCapturingTest { + private final PrintStream originalStdout = System.out; + private final PrintStream originalStderr = System.err; + protected OutputStream stdout; + protected OutputStream stderr; + protected ListAppender logged; + + @AfterEach + public void tearDown() { + + System.setOut(originalStdout); + System.setErr(originalStderr); + } + + @BeforeEach + public void setUp() { + stdout = captureStdout(); + stderr = captureStderr(); + logged = captureLog(Level.DEBUG, "nl.knaw.dans"); + } + + public static ListAppender captureLog(Level error, String loggerName) { + var logger = (Logger) LoggerFactory.getLogger(loggerName); + ListAppender listAppender = new ListAppender<>(); + listAppender.start(); + logger.setLevel(error); + logger.addAppender(listAppender); + return listAppender; + } + + public static ByteArrayOutputStream captureStdout() { + var outContent = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outContent)); + return outContent; + } + + public static ByteArrayOutputStream captureStderr() { + var outContent = new ByteArrayOutputStream(); + System.setErr(new PrintStream(outContent)); + return outContent; + } +} diff --git a/src/test/java/nl/knaw/dans/dvcli/AbstractTestWithTestDir.java b/src/test/java/nl/knaw/dans/dvcli/AbstractTestWithTestDir.java new file mode 100644 index 0000000..69b9354 --- /dev/null +++ b/src/test/java/nl/knaw/dans/dvcli/AbstractTestWithTestDir.java @@ -0,0 +1,36 @@ +package nl.knaw.dans.dvcli;/* + * Copyright (C) 2024 DANS - Data Archiving and Networked Services (info@dans.knaw.nl) + * + * 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. + */ + +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.BeforeEach; + +import java.nio.file.Path; + +/** + * A test class that creates a test directory for each test method. + */ +public abstract class AbstractTestWithTestDir { + protected final Path testDir = Path.of("target/test") + .resolve(getClass().getSimpleName()); + + @BeforeEach + public void setUp() throws Exception { + if (testDir.toFile().exists()) { + // github stumbled: https://github.com/DANS-KNAW/dans-layer-store-lib/actions/runs/8705753485/job/23876831089?pr=7#step:4:106 + FileUtils.deleteDirectory(testDir.toFile()); + } + } +} diff --git a/src/test/java/nl/knaw/dans/dvcli/action/BatchProcessorTest.java b/src/test/java/nl/knaw/dans/dvcli/action/BatchProcessorTest.java new file mode 100644 index 0000000..babfb80 --- /dev/null +++ b/src/test/java/nl/knaw/dans/dvcli/action/BatchProcessorTest.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2024 DANS - Data Archiving and Networked Services (info@dans.knaw.nl) + * + * 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 nl.knaw.dans.dvcli.action; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import nl.knaw.dans.dvcli.AbstractCapturingTest; +import nl.knaw.dans.lib.dataverse.DatasetApi; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class BatchProcessorTest extends AbstractCapturingTest { + public static Stream messagesOf(ListAppender logged) { + return logged.list.stream().map(iLoggingEvent -> iLoggingEvent.getLevel() + " " + iLoggingEvent.getFormattedMessage()); + } + + @Test + public void batchProcessor_should_continue_after_failure() { + var mockedDatasetApi = Mockito.mock(DatasetApi.class); + + BatchProcessor. builder() + .labeledItems(List.of( + new Pair<>("a", Mockito.mock(DatasetApi.class)), + new Pair<>("b", mockedDatasetApi), + new Pair<>("c", Mockito.mock(DatasetApi.class)) + )) + .action(datasetApi -> { + if (!datasetApi.equals(mockedDatasetApi)) + return "ok"; + else + throw new RuntimeException("test"); + }) + .report(new ConsoleReport<>()) + .delay(1L) + .build() + .process(); + + assertThat(stderr.toString()) + .isEqualTo(""" + a: OK. b: FAILED: Exception type = RuntimeException, message = test + c: OK.""" + " "); // java text block trims trailing spaces + assertThat(stdout.toString()).isEqualTo(""" + INFO Starting batch processing + INFO Processing item 1 of 3 + ok + DEBUG Sleeping for 1 ms + INFO Processing item 2 of 3 + DEBUG Sleeping for 1 ms + INFO Processing item 3 of 3 + ok + INFO Finished batch processing of 3 items + """); + assertThat(messagesOf(logged)) + .containsExactly("INFO Starting batch processing", + "INFO Processing item 1 of 3", + "DEBUG Sleeping for 1 ms", + "INFO Processing item 2 of 3", + "DEBUG Sleeping for 1 ms", + "INFO Processing item 3 of 3", + "INFO Finished batch processing of 3 items"); + } + + @Test + public void batchProcessor_sleep_a_default_amount_of_time_only_between_processing() { + BatchProcessor. builder() + .labeledItems(List.of( + new Pair<>("a", Mockito.mock(DatasetApi.class)), + new Pair<>("b", Mockito.mock(DatasetApi.class)), + new Pair<>("c", Mockito.mock(DatasetApi.class)) + )) + .action(datasetApi -> "ok") + .report(new ConsoleReport<>()) + .build() + .process(); + + assertThat(stderr.toString()) + .isEqualTo("a: OK. b: OK. c: OK. "); + assertThat(stdout.toString()).isEqualTo(""" + INFO Starting batch processing + INFO Processing item 1 of 3 + ok + DEBUG Sleeping for 1000 ms + INFO Processing item 2 of 3 + ok + DEBUG Sleeping for 1000 ms + INFO Processing item 3 of 3 + ok + INFO Finished batch processing of 3 items + """); + } + + @Test + public void batchProcessor_should_not_report_sleeping() { + BatchProcessor. builder() + .labeledItems(List.of( + new Pair<>("A", Mockito.mock(DatasetApi.class)), + new Pair<>("B", Mockito.mock(DatasetApi.class)), + new Pair<>("C", Mockito.mock(DatasetApi.class)) + )) + .action(datasetApi -> "ok") + .delay(0L) + .report(new ConsoleReport<>()) + .build() + .process(); + + assertThat(stderr.toString()).isEqualTo("A: OK. B: OK. C: OK. "); + assertThat(stdout.toString()).isEqualTo(""" + INFO Starting batch processing + INFO Processing item 1 of 3 + ok + INFO Processing item 2 of 3 + ok + INFO Processing item 3 of 3 + ok + INFO Finished batch processing of 3 items + """); + } + + @Test + public void batchProcessor_uses_a_default_report() { + BatchProcessor. builder() + .labeledItems(List.of( + new Pair<>("X", Mockito.mock(DatasetApi.class)), + new Pair<>("Y", Mockito.mock(DatasetApi.class)), + new Pair<>("Z", Mockito.mock(DatasetApi.class)) + )) + .action(datasetApi -> "ok") + .delay(0L) + .build() + .process(); + + assertThat(stderr.toString()).isEqualTo("X: OK. Y: OK. Z: OK. "); + assertThat(stdout.toString()).isEqualTo(""" + INFO Starting batch processing + INFO Processing item 1 of 3 + ok + INFO Processing item 2 of 3 + ok + INFO Processing item 3 of 3 + ok + INFO Finished batch processing of 3 items + """); + } + + @Test + public void batchProcessor_reports_empty_list() { + BatchProcessor. builder() + .labeledItems(List.of()) + .action(datasetApi -> "ok") + .report(new ConsoleReport<>()) + .build() + .process(); + + assertThat(stderr.toString()).isEqualTo(""); + assertThat(stdout.toString()).isEqualTo(""" + INFO Starting batch processing + INFO Finished batch processing of 0 items + """); + assertThat(messagesOf(logged)).containsExactly( + "INFO Starting batch processing", + "INFO Finished batch processing of 0 items"); + } + + @Test + public void batchProcessor_throws_on_missing_list() { + var processor = BatchProcessor. builder() + .action(datasetApi -> "ok") + .report(new ConsoleReport<>()); + + assertThatThrownBy(processor::build) + .isInstanceOf(NullPointerException.class) + .hasMessage("labeledItems is marked non-null but is null"); + + assertThat(stderr.toString()).isEqualTo(""); + assertThat(stdout.toString()).isEqualTo(""); + assertThat(messagesOf(logged)).containsExactly(); + } + + @Test + public void batchProcessor_fails_fast_on_missing_action() { + var processor = BatchProcessor. builder() + .labeledItems(List.of()); + assertThatThrownBy(processor::build) + .isInstanceOf(NullPointerException.class) + .hasMessage("action is marked non-null but is null"); + + assertThat(stderr.toString()).isEqualTo(""); + assertThat(stdout.toString()).isEqualTo(""); + assertThat(messagesOf(logged)).containsExactly(); + } +} diff --git a/src/test/java/nl/knaw/dans/dvcli/action/SingleOrTest.java b/src/test/java/nl/knaw/dans/dvcli/action/SingleOrTest.java new file mode 100644 index 0000000..e91bf33 --- /dev/null +++ b/src/test/java/nl/knaw/dans/dvcli/action/SingleOrTest.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2024 DANS - Data Archiving and Networked Services (info@dans.knaw.nl) + * + * 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 nl.knaw.dans.dvcli.action; + +import nl.knaw.dans.dvcli.AbstractTestWithTestDir; +import nl.knaw.dans.lib.dataverse.DatasetApi; +import nl.knaw.dans.lib.dataverse.DataverseClient; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.List; +import java.util.stream.Stream; + +import static nl.knaw.dans.dvcli.action.SingleIdOrIdsFile.DEFAULT_TARGET_PLACEHOLDER; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +@SuppressWarnings({ "unchecked", "rawtypes" }) // for mapSecondToString +public class SingleOrTest extends AbstractTestWithTestDir { + // SingleDatasetOrDatasetsFile implicitly tests SingleIdOrIdsFile + // SingleCollectionOrCollectionsFile too and has little to add + + private final InputStream originalStdin = System.in; + + @AfterEach + public void tearDown() { + System.setIn(originalStdin); + } + + public static Stream> mapSecondToString(List> collections) { + return collections.stream().map(p -> new Pair(p.getFirst(), p.getSecond().toString())); + } + + @Test + public void getCollections_should_return_single_value() throws Exception { + + var collections = new SingleCollectionOrCollectionsFile("xyz", new DataverseClient(null)) + .getCollections().toList(); + + assertThat(mapSecondToString(collections)).containsExactly( + new Pair("xyz", "DataverseApi(subPath=api/dataverses/xyz)") + ); + } + + @Test + public void getPids_should_return_placeHolder() throws Exception { + var pids = new SingleIdOrIdsFile(DEFAULT_TARGET_PLACEHOLDER, "default") + .getPids(); + Assertions.assertThat(pids) + .containsExactlyInAnyOrderElementsOf(List.of("default")); + } + + @Test + public void getDatasetIds_should_return_single_dataset_in_aList() throws Exception { + var datasets = new SingleDatasetOrDatasetsFile("1", new DataverseClient(null)) + .getDatasets().toList(); + assertThat(mapSecondToString(datasets)) + .containsExactly(new Pair("1", "DatasetApi(id='1, isPersistentId=false)")); + } + + @Test + public void getDatasets_should_parse_file_with_white_space() throws Exception { + + var filePath = testDir.resolve("ids.txt"); + Files.createDirectories(testDir); + Files.writeString(filePath, """ + a blabla + 1"""); + + var datasets = new SingleDatasetOrDatasetsFile(filePath.toString(), new DataverseClient(null)) + .getDatasets().toList(); + + assertThat(mapSecondToString(datasets)).containsExactly( + new Pair("a", "DatasetApi(id='a, isPersistentId=true)"), + new Pair("blabla", "DatasetApi(id='blabla, isPersistentId=true)"), + new Pair("1", "DatasetApi(id='1, isPersistentId=false)") + ); + } + + @Test + public void getDatasets_should_throw_when_parsing_a_directory() { + + var ids = new SingleDatasetOrDatasetsFile("src/test/resources", new DataverseClient(null)); + assertThatThrownBy(ids::getDatasets) + .isInstanceOf(IOException.class) + .hasMessage("src/test/resources is not a regular file"); + } + + @Test + public void getDatasets_should_parse_stdin_and_return_empty_lines() throws Exception { + + System.setIn(new ByteArrayInputStream(""" + A + + B rabarbera + + """.getBytes())); + + var datasets = new SingleDatasetOrDatasetsFile("-", new DataverseClient(null)) + .getDatasets().toList(); + assertThat(mapSecondToString(datasets)).containsExactly( + new Pair("A", "DatasetApi(id='A, isPersistentId=true)"), + new Pair("", "DatasetApi(id=', isPersistentId=true)"), + new Pair("B", "DatasetApi(id='B, isPersistentId=true)"), + new Pair("rabarbera", "DatasetApi(id='rabarbera, isPersistentId=true)"), + new Pair("", "DatasetApi(id=', isPersistentId=true)") + ); + } + + @Test + @SuppressWarnings("ResultOfMethodCallIgnored") // for toList in assertThatThrownBy + public void getDatasets_should_read_until_Exception() throws Exception { + + var dataverseClient = mock(DataverseClient.class); + + Mockito.when(dataverseClient.dataset("A")) + .thenReturn(mock(DatasetApi.class)); + Mockito.when(dataverseClient.dataset("whoops")) + .thenThrow(new RuntimeException("test")); + + System.setIn(new ByteArrayInputStream(""" + A + whoops + B""".getBytes())); + + var datasets = new SingleDatasetOrDatasetsFile("-", dataverseClient) + .getDatasets(); + assertThatThrownBy(datasets::toList) + .isInstanceOf(RuntimeException.class) + .hasMessage("test"); + + verify(dataverseClient, times(1)).dataset("A"); + verify(dataverseClient, times(1)).dataset("whoops"); + verify(dataverseClient, times(0)).dataset("B"); + verifyNoMoreInteractions(dataverseClient); + } +} diff --git a/src/test/java/nl/knaw/dans/dvcli/command/AbstractSubcommandContainerTest.java b/src/test/java/nl/knaw/dans/dvcli/command/AbstractSubcommandContainerTest.java new file mode 100644 index 0000000..9f41ddc --- /dev/null +++ b/src/test/java/nl/knaw/dans/dvcli/command/AbstractSubcommandContainerTest.java @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2024 DANS - Data Archiving and Networked Services (info@dans.knaw.nl) + * + * 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 nl.knaw.dans.dvcli.command; + +import nl.knaw.dans.dvcli.AbstractCapturingTest; +import nl.knaw.dans.dvcli.action.Pair; +import nl.knaw.dans.lib.dataverse.DataverseClient; +import nl.knaw.dans.lib.dataverse.DataverseException; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +public class AbstractSubcommandContainerTest extends AbstractCapturingTest { + private static final Logger log = LoggerFactory.getLogger(AbstractSubcommandContainerTest.class); + + private static class TestCmd extends AbstractSubcommandContainer { + + public TestCmd(String targets) { + super(new DataverseClient(null)); + this.targets = targets; + } + + @Override + protected List> getItems() throws IOException { + return List.of( + new Pair<>("1", "value of 1") + ); + } + } + + @Test + public void datasetCmd_with_dir_as_targets_file_throws() throws Exception { + + var cmd = new DatasetCmd(new DataverseClient(null)){ + @Override + public void doCall() throws IOException { + getItems(); + } + }; + + // set private field + var targetField = AbstractSubcommandContainer.class.getDeclaredField("targets"); + targetField.setAccessible(true); + targetField.set(cmd, "src/test/resources"); + + assertThatThrownBy(cmd::doCall) + .isInstanceOf(IOException.class) + .hasMessage("src/test/resources is not a regular file"); + + assertThat(logged.list).isEmpty(); + assertThat(stdout.toString()).isEqualTo(""); + assertThat(stderr.toString()).isEqualTo(""); + } + + @Test + public void collectionCmd_with_dir_as_targets_file_throws() throws Exception { + + var cmd = new CollectionCmd(new DataverseClient(null)) { + + @Override + public void doCall() throws IOException { + log.debug("doCall"); + getItems(); + } + }; + + // set private field + var targetField = AbstractSubcommandContainer.class.getDeclaredField("targets"); + targetField.setAccessible(true); + targetField.set(cmd, "src/test/resources"); + + assertThatThrownBy(cmd::doCall) + .isInstanceOf(IOException.class) + .hasMessage("src/test/resources is not a regular file"); + + assertThat(stdout.toString()).isEqualTo("DEBUG doCall\n"); + assertThat(stderr.toString()).isEqualTo(""); + } + + @Test + public void call_should_return_one_on_a_dataverseException_by_doCall() throws Exception { + var cmd = new TestCmd("1") { + + @Override + public void doCall() throws DataverseException { + throw new DataverseException(999, "test"); + } + }; + + assertThat(cmd.call()).isEqualTo(1); + + assertThat(logged.list).isEmpty(); + assertThat(stdout.toString()).isEqualTo(""); + assertThat(stderr.toString()).isEqualTo(""" + status: 999; message: test + """); + } + + @Test + public void call_should_throw_on_an_ioException_by_doCall() { + var cmd = new TestCmd("1") { + + @Override + public void doCall() throws IOException { + throw new IOException("test"); + } + }; + + assertThatThrownBy(cmd::call) + .isInstanceOf(IOException.class) + .hasMessage("test"); + + assertThat(logged.list).isEmpty(); + assertThat(stdout.toString()).isEqualTo(""); + assertThat(stderr.toString()).isEqualTo(""); + } + + @Test + public void call_should_throw_on_an_runtimeException_by_doCall() { + var cmd = new TestCmd("1") { + + @Override + public void doCall() { + throw new RuntimeException("test"); + } + }; + + assertThatThrownBy(cmd::call) + .isInstanceOf(RuntimeException.class) + .hasMessage("test"); + + assertThat(logged.list).isEmpty(); + assertThat(stdout.toString()).isEqualTo(""); + assertThat(stderr.toString()).isEqualTo(""); + } + + @Test + public void call_should_return_zero_with_the_default_doCall_which_does_nothing() throws Exception { + var cmd = new TestCmd("1"); + + assertThat(cmd.call()).isEqualTo(0); + + assertThat(logged.list).isEmpty(); + assertThat(stdout.toString()).isEqualTo(""); + assertThat(stderr.toString()).isEqualTo(""); + } + + @Test + public void batchProcessorBuilder_throws_when_getItems_throws_ioException() { + var cmd = new TestCmd("1") { + + @Override + protected List> getItems() throws IOException { + throw new IOException("test"); + } + }; + + assertThatThrownBy(cmd::batchProcessorBuilder) + .isInstanceOf(IOException.class) + .hasMessage("test"); + + assertThat(logged.list).isEmpty(); + assertThat(stdout.toString()).isEqualTo(""); + assertThat(stderr.toString()).isEqualTo(""); + } + + @Test + public void batchProcessorBuilder_throws_when_getItems_throws_runtimeException() { + var cmd = new TestCmd("1") { + + @Override + protected List> getItems() { + throw new RuntimeException("test"); + } + }; + + assertThatThrownBy(cmd::batchProcessorBuilder) + .isInstanceOf(RuntimeException.class) + .hasMessage("test"); + + assertThat(logged.list).isEmpty(); + assertThat(stdout.toString()).isEqualTo(""); + assertThat(stderr.toString()).isEqualTo(""); + } + + @Test + public void batchProcessorBuilder_can_build_without_labeled_items() throws Exception { + var builder = new TestCmd("1") + .batchProcessorBuilder() + .action(Object::toString); + + assertDoesNotThrow(builder::build); + + assertThat(logged.list).isEmpty(); + assertThat(stdout.toString()).isEqualTo(""); + assertThat(stderr.toString()).isEqualTo(""); + } + + @Test + public void paramsBatchProcessorBuilder_requires_labeled_items() { + var builder = new TestCmd("1") + .paramsBatchProcessorBuilder() + .action(Object::toString); + + assertThatThrownBy(builder::build) + .isInstanceOf(NullPointerException.class) + .hasMessage("labeledItems is marked non-null but is null"); + + assertThat(logged.list).isEmpty(); + assertThat(stdout.toString()).isEqualTo(""); + assertThat(stderr.toString()).isEqualTo(""); + } + + @Test + public void paramsBatchProcessorBuilder_can_build_with_labeled_items() throws Exception { + var cmd = new TestCmd("1"); + var builder = cmd + .paramsBatchProcessorBuilder() + .labeledItems(cmd.getItems()) + .action(Object::toString); + + assertDoesNotThrow(builder::build); + + assertThat(logged.list).isEmpty(); + assertThat(stdout.toString()).isEqualTo(""); + assertThat(stderr.toString()).isEqualTo(""); + } + + @Test + public void batchProcessor_does_not_throw() { + assertDoesNotThrow(() -> + new TestCmd("1") + .batchProcessor(Object::toString) + .process() + ); + + assertThat(stderr.toString()).isEqualTo("1: OK. "); + assertThat(stdout.toString()).isEqualTo(""" + INFO Starting batch processing + INFO Processing item 1 of 1 + value of 1 + INFO Finished batch processing of 1 items + """); + assertThat(logged.list.stream().map(Object::toString).toList()).containsExactly( + "[INFO] Starting batch processing", + "[INFO] Processing item 1 of 1", + "[INFO] Finished batch processing of 1 items" + ); + } +} diff --git a/src/test/java/nl/knaw/dans/dvcli/command/CollectionCreateDatasetTest.java b/src/test/java/nl/knaw/dans/dvcli/command/CollectionCreateDatasetTest.java new file mode 100644 index 0000000..2d116a4 --- /dev/null +++ b/src/test/java/nl/knaw/dans/dvcli/command/CollectionCreateDatasetTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2024 DANS - Data Archiving and Networked Services (info@dans.knaw.nl) + * + * 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 nl.knaw.dans.dvcli.command; + +import nl.knaw.dans.dvcli.AbstractCapturingTest; +import nl.knaw.dans.lib.dataverse.DataverseApi; +import nl.knaw.dans.lib.dataverse.DataverseClient; +import nl.knaw.dans.lib.dataverse.DataverseClientConfig; +import nl.knaw.dans.lib.dataverse.DataverseHttpResponse; +import nl.knaw.dans.lib.dataverse.model.dataset.DatasetCreationResult; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +public class CollectionCreateDatasetTest extends AbstractCapturingTest { + + private final InputStream originalStdin = System.in; + + @AfterEach + public void tearDown() { + System.setIn(originalStdin); + } + + @Test + public void doCall_continues_on_unknownHost() throws Exception { + + var unknownHostConfig = new DataverseClientConfig(new URI("https://does.not.exist.dans.knaw.nl"), "apiToken"); + var client = new DataverseClient(unknownHostConfig); + + var metadataKeys = new HashMap(); + var json = "src/test/resources/debug-etc/config.yml"; // invalid json file, don't care + System.setIn(new ByteArrayInputStream("A B C".getBytes())); + + // command under test + var cmd = getCmd("-", metadataKeys, json, client); + cmd.doCall(); + + assertThat(stdout.toString()).isEqualTo(""" + INFO Starting batch processing + INFO Processing item 1 of 3 + DEBUG buildUri: https://does.not.exist.dans.knaw.nl/api/dataverses/A/datasets + INFO Processing item 2 of 3 + DEBUG buildUri: https://does.not.exist.dans.knaw.nl/api/dataverses/B/datasets + INFO Processing item 3 of 3 + DEBUG buildUri: https://does.not.exist.dans.knaw.nl/api/dataverses/C/datasets + INFO Finished batch processing of 3 items + """); + assertThat(stderr.toString()).isEqualTo(""" + A: FAILED: Exception type = UnknownHostException, message = does.not.exist.dans.knaw.nl: Name or service not known + B: FAILED: Exception type = UnknownHostException, message = does.not.exist.dans.knaw.nl + C: FAILED: Exception type = UnknownHostException, message = does.not.exist.dans.knaw.nl + """); // TODO implement fail fast in BatchProcessor for these type of exceptions? + } + + @Test + public void doCall_is_happy() throws Exception { + + var metadataKeys = new HashMap(); + var jsonFile = "src/test/resources/debug-etc/config.yml"; // invalid json file, don't care + + // mock objects + @SuppressWarnings("unchecked") + DataverseHttpResponse response = Mockito.mock(DataverseHttpResponse.class); + var client = Mockito.mock(DataverseClient.class); + var api = Mockito.mock(DataverseApi.class); + + // mock behavior + Mockito.when(client.dataverse("A")).thenReturn(api); + Mockito.when(api.createDataset(Files.readString(Path.of(jsonFile)), metadataKeys)).thenReturn(response); + Mockito.when(response.getEnvelopeAsString()).thenReturn("mock response"); + + // command under test + CollectionCreateDataset cmd = getCmd("A", metadataKeys, jsonFile, client); + cmd.doCall(); + + assertThat(stderr.toString()).isEqualTo("A: OK. "); + assertThat(stdout.toString()).isEqualTo(""" + INFO Starting batch processing + INFO Processing item 1 of 1 + mock response + INFO Finished batch processing of 1 items + """); + + verify(api, times(1)).createDataset((String) any(), any()); + verify(response, times(1)).getEnvelopeAsString(); + verify(client, times(1)).dataverse(any()); + verifyNoMoreInteractions(api); + } + + @Test + public void doCall_with_dir_as_json_file_fails() throws Exception { + + var metadataKeys = new HashMap(); + var jsonFile = "src/test/resources/debug-etc"; + var client = new DataverseClient(new DataverseClientConfig(null)); + var originalIn = System.in; + System.setIn(new ByteArrayInputStream("A B".getBytes())); + + try { + // command under test + CollectionCreateDataset cmd = getCmd("-", metadataKeys, jsonFile, client); + cmd.doCall(); + } + finally { + System.setIn(originalIn); + } + + assertThat(stderr.toString()).isEqualTo(""" + A: FAILED: Exception type = IOException, message = Is a directory + B: FAILED: Exception type = IOException, message = Is a directory + """); + assertThat(stdout.toString()).isEqualTo(""" + INFO Starting batch processing + INFO Processing item 1 of 2 + INFO Processing item 2 of 2 + INFO Finished batch processing of 2 items + """); + } + + private static CollectionCreateDataset getCmd(String target, HashMap metadataKeys, String json, final DataverseClient client) + throws NoSuchFieldException, IllegalAccessException { + + // set private fields with reflection + + var cmd = new CollectionCmd(client); + + var targetsField = AbstractSubcommandContainer.class.getDeclaredField("targets"); + targetsField.setAccessible(true); + targetsField.set(cmd, target); + + var subCmd = new CollectionCreateDataset(); + + var datasetField = CollectionCreateDataset.class.getDeclaredField("dataset"); + datasetField.setAccessible(true); + datasetField.set(subCmd, json); + + var metadataKeysField = CollectionCreateDataset.class.getDeclaredField("metadataKeys"); + metadataKeysField.setAccessible(true); + metadataKeysField.set(subCmd, metadataKeys); + + var collectionCmdField = CollectionCreateDataset.class.getDeclaredField("collectionCmd"); + collectionCmdField.setAccessible(true); + collectionCmdField.set(subCmd, cmd); + + return subCmd; + } +}