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