diff --git a/README.md b/README.md index 316ba71..8382a55 100644 --- a/README.md +++ b/README.md @@ -26,4 +26,4 @@ Any valid study code can be entered. Some studies are preemptively hard-coded (` Unknown studies will be newly created. Records about existing studies are stored in the directory `./.imicodes/`. That directory contains text files which are named after studies. -Each study file contains the number of the last code which was generated, so new codes will have different numbers by incrementing the last code by one. +Each study file contains the number of the next code which will be generated, so new codes will have different numbers by incrementing the last code by one. diff --git a/src/main/java/de/vanitasvitae/imi/codes/CodeGenerator.java b/src/main/java/de/vanitasvitae/imi/codes/CodeGenerator.java index 701bc05..c3996a4 100644 --- a/src/main/java/de/vanitasvitae/imi/codes/CodeGenerator.java +++ b/src/main/java/de/vanitasvitae/imi/codes/CodeGenerator.java @@ -6,6 +6,7 @@ import java.util.List; import de.vanitasvitae.imi.codes.input.InvalidOptionException; import de.vanitasvitae.imi.codes.persistence.FileRepository; +import de.vanitasvitae.imi.codes.persistence.Repository; import de.vanitasvitae.imi.codes.types.SampleTubeCode; import de.vanitasvitae.imi.codes.types.SampleType; import de.vanitasvitae.imi.codes.types.StudyNumber; @@ -34,23 +35,27 @@ public class CodeGenerator { * @throws InvalidOptionException if too many codes would be generated. * @throws IOException if IO goes wrong reading or writing sample code records. */ - public static List generateCodes(FileRepository repository, StudyNumber studyNumber, SampleType sampleType, int numberOfCodes) + public static List generateCodes(Repository repository, StudyNumber studyNumber, SampleType sampleType, int numberOfCodes) throws InvalidOptionException, IOException { + + if (numberOfCodes < 1) { + throw new InvalidOptionException("Number of codes cannot be less than 1."); + } List codes = new ArrayList<>(); // Read the next sample code from file - int nextSampleCode = repository.nextSampleCode(studyNumber); + int nextSampleCode = repository.getNextSampleCode(studyNumber); int nextTotal = nextSampleCode + numberOfCodes; // Check, if we'd have an overflow of sample numbers // We check like this to prevent integer overflows if (nextSampleCode > 9999 || numberOfCodes > 9999 || nextTotal - 1 > 9999) { - throw new InvalidOptionException("Study " + studyNumber + "would have too many sample tubes" + + throw new InvalidOptionException("Study " + studyNumber + " would have too many sample tubes" + " (" + (nextTotal - 1) + "). Aborting."); } // Write back the number of the next sample number that should be generated next time - repository.writeNextSampleCode(studyNumber, nextSampleCode + numberOfCodes); + repository.setNextSampleCode(studyNumber, nextSampleCode + numberOfCodes); // Generate codes for (int i = 0; i < numberOfCodes; i++) { diff --git a/src/main/java/de/vanitasvitae/imi/codes/persistence/FileRepository.java b/src/main/java/de/vanitasvitae/imi/codes/persistence/FileRepository.java index e0dc588..82a2f13 100644 --- a/src/main/java/de/vanitasvitae/imi/codes/persistence/FileRepository.java +++ b/src/main/java/de/vanitasvitae/imi/codes/persistence/FileRepository.java @@ -22,7 +22,7 @@ import de.vanitasvitae.imi.codes.types.StudyNumber; * If the user tries to generate codes for those studies, the generated codes start at the hard coded values instead * of 0. */ -public class FileRepository { +public class FileRepository implements Repository { private final File base; private final HashMap hardCoded = new HashMap<>(); @@ -65,7 +65,8 @@ public class FileRepository { * @param study code for the study * @return sample number */ - public int nextSampleCode(StudyNumber study) { + @Override + public int getNextSampleCode(StudyNumber study) { File studyNumberFile = new File(base, study.toString()); int next; // Check, if we have hard-coded values available @@ -88,7 +89,8 @@ public class FileRepository { * * @throws IOException in case the file cannot be written */ - public void writeNextSampleCode(StudyNumber study, int nextSampleCode) throws IOException { + @Override + public void setNextSampleCode(StudyNumber study, int nextSampleCode) throws IOException { writeInt(new File(base, study.toString()), nextSampleCode); } diff --git a/src/main/java/de/vanitasvitae/imi/codes/persistence/Repository.java b/src/main/java/de/vanitasvitae/imi/codes/persistence/Repository.java new file mode 100644 index 0000000..8224f17 --- /dev/null +++ b/src/main/java/de/vanitasvitae/imi/codes/persistence/Repository.java @@ -0,0 +1,13 @@ +package de.vanitasvitae.imi.codes.persistence; + +import java.io.IOException; + +import de.vanitasvitae.imi.codes.types.StudyNumber; + +public interface Repository { + + int getNextSampleCode(StudyNumber studyNumber); + + void setNextSampleCode(StudyNumber studyNumber, int sampleCode) throws IOException; + +} diff --git a/src/test/java/de/vanitasvitae/imi/codes/CodeGeneratorTest.java b/src/test/java/de/vanitasvitae/imi/codes/CodeGeneratorTest.java new file mode 100644 index 0000000..70fe4a3 --- /dev/null +++ b/src/test/java/de/vanitasvitae/imi/codes/CodeGeneratorTest.java @@ -0,0 +1,85 @@ +package de.vanitasvitae.imi.codes; + +import static junit.framework.TestCase.assertEquals; + +import java.io.IOException; +import java.util.List; + +import de.vanitasvitae.imi.codes.input.InvalidOptionException; +import de.vanitasvitae.imi.codes.persistence.Repository; +import de.vanitasvitae.imi.codes.persistence.SimpleTestRepository; +import de.vanitasvitae.imi.codes.types.SampleTubeCode; +import de.vanitasvitae.imi.codes.types.SampleType; +import de.vanitasvitae.imi.codes.types.StudyNumber; +import org.junit.Before; +import org.junit.Test; + +public class CodeGeneratorTest { + + // Test Repository, which stores sample code records in an ephemeral HashMap, so no cleaning is needed :) + private static final Repository repository = new SimpleTestRepository(); + + @Before + public void populateRepository() throws InvalidOptionException, IOException { + // Put some dummy values into the repository. + repository.setNextSampleCode(new StudyNumber("AAA"), 42); + repository.setNextSampleCode(new StudyNumber("BBB"), 56); + repository.setNextSampleCode(new StudyNumber("ZZZ"), 9999); + } + + /** + * Check, if generated codes match expected output. + * + * @throws InvalidOptionException NOT expected since the input should be valid. + * @throws IOException NOT expected, since we do no IO. + */ + @Test + public void testGenerateCodes() throws InvalidOptionException, IOException { + List codes = CodeGenerator.generateCodes(repository, new StudyNumber("AAA"), new SampleType("b"), 34); + + assertEquals("The size of the list of generated codes must match.", 34, codes.size()); + assertEquals("The first generated code must match the expected format.", "AAAb0042", codes.get(0).toString()); + + for (int i = 1; i < codes.size(); i++) { + assertEquals("All codes in the list must match the expected codes.", + String.format("AAAb%04d", (42 + i)), codes.get(i).toString()); + } + } + + /** + * Test whether the method throws an exception, when there would be too many codes generated for a study. + * This can happen, if a study has n codes already generated, and the user tries to generate m additional codes, so + * that n+m > 9999. This must not happen, since we only have 4 decimal letters to encode the sample number. + * + * @throws InvalidOptionException expected. + * @throws IOException NOT expected, since we do not IO. + */ + @Test (expected = InvalidOptionException.class) + public void testGenerateCodesFailsWhenOverflowing() throws InvalidOptionException, IOException { + CodeGenerator.generateCodes(repository, new StudyNumber("ZZZ"), new SampleType("b"), 2); + } + + /** + * Test whether the method fails for too big numbers of codes to generate. + * This is done to prevent overflows similar as in {@link #testGenerateCodesFailsWhenOverflowing()}. + * + * @throws InvalidOptionException expected. + * @throws IOException NOT expected, since we do no IO. + */ + @Test (expected = InvalidOptionException.class) + public void testGenerateCodesFailsForTooHighNumberOfCodes() throws InvalidOptionException, IOException { + CodeGenerator.generateCodes(repository, new StudyNumber("abc"), new SampleType("x"), 10001); + } + + /** + * Test whether the method throws an exception for too low number of codes to be generated. + * The number of codes to be generated must be at least 1. + * + * @throws InvalidOptionException expected. + * @throws IOException NOT expected (no IO). + */ + @Test (expected = InvalidOptionException.class) + public void testGenerateCodesFailsForTooLowNumberOfCodes() throws InvalidOptionException, IOException { + CodeGenerator.generateCodes(repository, new StudyNumber("abc"), new SampleType("x"), -4); + } +} diff --git a/src/test/java/de/vanitasvitae/imi/codes/input/InputValidatorTest.java b/src/test/java/de/vanitasvitae/imi/codes/input/InputValidatorTest.java index 3629723..33114d5 100644 --- a/src/test/java/de/vanitasvitae/imi/codes/input/InputValidatorTest.java +++ b/src/test/java/de/vanitasvitae/imi/codes/input/InputValidatorTest.java @@ -10,6 +10,7 @@ import de.vanitasvitae.imi.codes.input.InputValidator; import de.vanitasvitae.imi.codes.input.InvalidOptionException; import de.vanitasvitae.imi.codes.types.SampleType; import de.vanitasvitae.imi.codes.types.StudyNumber; +import jdk.internal.util.xml.impl.Input; import org.junit.Test; public class InputValidatorTest { @@ -99,6 +100,27 @@ public class InputValidatorTest { } } + /** + * {@link SampleType SampleTypes} must be of length 1. This test checks, whether + * {@link InputValidator#validateSampleType(String)} fails for inputs of length 2. + * + * @throws InvalidOptionException expected. + */ + @Test (expected = InvalidOptionException.class) + public void testValidateSampleTypeFailsForTooLongSampleTypes() throws InvalidOptionException { + InputValidator.validateSampleType("aa"); + } + + /** + * Test whether {@link InputValidator#validateSampleType(String)} fails for inputs from the wrong alphabet. + * + * @throws InvalidOptionException expected. + */ + @Test (expected = InvalidOptionException.class) + public void testValidateSampleTypeFailsForInvalidSampleTypes() throws InvalidOptionException { + InputValidator.validateSampleType("$"); + } + /** * Test, whether {@link InputValidator#validateNumberOfCodes(String)} returns the expected {@link Integer}. * In this method, posit diff --git a/src/test/java/de/vanitasvitae/imi/codes/persistence/FileRepositoryTest.java b/src/test/java/de/vanitasvitae/imi/codes/persistence/FileRepositoryTest.java index 6e3b4e3..ec2625c 100644 --- a/src/test/java/de/vanitasvitae/imi/codes/persistence/FileRepositoryTest.java +++ b/src/test/java/de/vanitasvitae/imi/codes/persistence/FileRepositoryTest.java @@ -7,7 +7,6 @@ import java.io.IOException; import java.util.Stack; import de.vanitasvitae.imi.codes.input.InvalidOptionException; -import de.vanitasvitae.imi.codes.persistence.FileRepository; import de.vanitasvitae.imi.codes.types.StudyNumber; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -36,22 +35,22 @@ public class FileRepositoryTest { public void testReadWriteSampleNumbers() throws InvalidOptionException, IOException { StudyNumber studyNumber = new StudyNumber("abc"); assertEquals("An unknown study number must return 0 as next sample number", - 0, repository.nextSampleCode(studyNumber)); + 0, repository.getNextSampleCode(studyNumber)); // The written next sample code must be returned next time int next = 34; - repository.writeNextSampleCode(studyNumber, next); + repository.setNextSampleCode(studyNumber, next); assertEquals("The written next sample number must be read next time.", - next, repository.nextSampleCode(studyNumber)); + next, repository.getNextSampleCode(studyNumber)); // The FileRepository contains 35 as hard coded sample number for study "AAA". StudyNumber hardCoded = new StudyNumber("AAA"); assertEquals("The hard coded value must be returned the first time its being read.", - 35, repository.nextSampleCode(hardCoded)); - repository.writeNextSampleCode(hardCoded, 89); + 35, repository.getNextSampleCode(hardCoded)); + repository.setNextSampleCode(hardCoded, 89); assertEquals("If the hard coded value is overwritten, the new value must be read.", - 89, repository.nextSampleCode(hardCoded)); + 89, repository.getNextSampleCode(hardCoded)); } @AfterClass diff --git a/src/test/java/de/vanitasvitae/imi/codes/persistence/SimpleTestRepository.java b/src/test/java/de/vanitasvitae/imi/codes/persistence/SimpleTestRepository.java new file mode 100644 index 0000000..0bd9873 --- /dev/null +++ b/src/test/java/de/vanitasvitae/imi/codes/persistence/SimpleTestRepository.java @@ -0,0 +1,27 @@ +package de.vanitasvitae.imi.codes.persistence; + +import java.util.HashMap; +import java.util.Map; + +import de.vanitasvitae.imi.codes.types.StudyNumber; + +/** + * Simple implementation of the {@link Repository} interface. + * Use this only for testing purposes, as no values are persisted. + */ +public class SimpleTestRepository implements Repository { + + private final Map sampleCodes = new HashMap<>(); + + @Override + public int getNextSampleCode(StudyNumber studyNumber) { + Integer sampleCode = sampleCodes.get(studyNumber); + + return sampleCode != null ? sampleCode : 0; + } + + @Override + public void setNextSampleCode(StudyNumber studyNumber, int sampleCode) { + sampleCodes.put(studyNumber, sampleCode); + } +}