diff --git a/sanofi/dist/etc/sanofi-dropbox/dropbox-all-in-one-with-library.py b/sanofi/dist/etc/sanofi-dropbox/dropbox-all-in-one-with-library.py index 7c9d0bb88377eb0627840cdc8317a3c0bc23b0ec..de8974b372d4b438b8b685271d86363eafa84edc 100644 --- a/sanofi/dist/etc/sanofi-dropbox/dropbox-all-in-one-with-library.py +++ b/sanofi/dist/etc/sanofi-dropbox/dropbox-all-in-one-with-library.py @@ -121,31 +121,62 @@ def findDir(incomingFile, dirNameMarker): return File(incomingPath, file) return None +""" Removes trailing empty strings from a list """ +def removeTrailingEmptyElements(list): + pos = len(list) + while (pos > 0): + pos = pos - 1 + if not list[pos].strip(): + del list[pos] + else: + break + return list + # ====================================== # end generic utility functions # ====================================== def rollback_service(service, ex): - plateCode = plate.getCode() - plateLink = createPlateLink(OPENBIS_URL, plateCode) + incomingFileName = incoming.getName() errorMessage = ex.getMessage() - sendEmail("openBIS: Data registration failed for %s" % (plateCode), """ + if not errorMessage: + errorMessage = ex.toString() + + if plateCode: + plateLink = createPlateLink(OPENBIS_URL, plateCode) + sendEmail("openBIS: Data registration failed for %s" % (plateCode), """ Dear openBIS user, Registering new data for plate %(plateLink)s has failed with error '%(errorMessage)s'. + The name of the incoming folder '%(incomingFileName)s' was added to '.faulty_paths'. Please, + repair the problem and remove the entry from '.faulty_paths' to retry registration. + + This email has been generated automatically. + + Administrator + """ % vars(), False) + else: + sendEmail("openBIS: Data registration failed for folder '%s'" % (incomingFileName), """ + Dear user, + + openBIS was unable to understand the name of an incoming folder. + Detailed error message was '%(errorMessage)s'. + This email has been generated automatically. Administrator - """ % vars(), False) + """ % vars(), False) + def commit_transaction(service, transaction): - plateCode = plate.getCode() + incomingFileName = incoming.getName() plateLink = createPlateLink(OPENBIS_URL, plateCode) sendEmail("openBIS: New data registered for %s" % (plateCode), """ Dear openBIS user, - New data for the plate %(plateLink)s has been registered. + New data from folder '%(incomingFileName)s' has been successfully registered in plate %(plateLink)s. + This email has been generated automatically. Have a nice day! @@ -196,15 +227,15 @@ def findPlateByCode(code): def parseIncomingDirname(dirName): """ Parses the name of an incoming dataset folder from the format - 'AcquisitionBatch_BarCode_Timestamp' to a tuple (acquisitionBatch, barCode) + '<ACQUISITION_BATCH_NAME>_<BAR_CODE>_<TIMESTAMP>' to a tuple (acquisitionBatch, plateCode) """ tokens = dirName.split("_") if len(tokens) < 2: - raise RuntimeError("Data set directory name does not match the pattern 'AcquisitionBatch_BarCode_Timestamp': " + dirName) + raise RuntimeError("Data set directory name does not match the pattern '<ACQUISITION_BATCH_NAME>_<BAR_CODE>_<TIMESTAMP>': " + dirName) acquisitionBatch = tokens[0] - barCode = tokens[1].split('.')[0] - return (acquisitionBatch, barCode) + plateCode = tokens[1].split('.')[0] + return (acquisitionBatch, plateCode) def removeDuplicates(list): dict = {} @@ -266,32 +297,28 @@ class PlateInitializer: def getWellCode(self, x, y): return ConversionUtils.convertToSpreadsheetLocation(Point(x,y)) - def getPlateDimensions(self): - """ - parses the plate geometry property from the form "384_WELLS_16X24" - to a tuple of integers (plateHeight, plateWidth) - """ + def getPlateGeometryDimensions(self): plateGeometryString = self.plate.getPropertyValue(ScreeningConstants.PLATE_GEOMETRY) geometry = Geometry.createFromPlateGeometryString(plateGeometryString) return (geometry.height, geometry.width) def validateLibraryDimensions(self, tsvLines): - (plateHeight, plateWidth) = self.getPlateDimensions() + (plateHeight, plateWidth) = self.getPlateGeometryDimensions() numLines = len(tsvLines) - if plateHeight != len(tsvLines) : - raise RuntimeError("The geometry property of plate %s (height=%s)" - " does not agree with the value of the %s" - " property in experiment %s (height=%s)." % \ - (self.plateCode, plateHeight, self.LIBRARY_TEMPLATE_PROPNAME, self.experimentId, numLines)) + if plateHeight < len(tsvLines) : + raise RuntimeError("The property %s of experiment '%s' contains %s rows, but the" + " geometry of plate '%s' allows a maximum of %s rows. You should either reduce" + " the number of rows in the library template or change the plate geometry." % + (self.LIBRARY_TEMPLATE_PROPNAME, self.experimentId, numLines, self.plateCode, plateHeight)) for i in range(0, len(tsvLines)): lineWidth = len(tsvLines[i]) - if plateWidth != lineWidth: - raise RuntimeError("The geometry property of plate %s (width=%s)" - " does not agree with the value of the %s" - " property in experiment %s (line=%s, width=%s)." % \ - (self.plateCode, plateWidth, self.LIBRARY_TEMPLATE_PROPNAME, self.experimentId, i, lineWidth)) + if plateWidth < lineWidth: + raise RuntimeError("The property %s of experiment '%s' contains %s columns in row %s, but the" + " geometry of plate '%s' allows a maximum of %s columns. You should either reduce" + " the number of columns in the library template or change the plate geometry." % + (self.LIBRARY_TEMPLATE_PROPNAME, self.experimentId, lineWidth, (i + 1), self.plateCode, plateHeight)) def parseLibraryTemplate(self): template = experiment.getPropertyValue(self.LIBRARY_TEMPLATE_PROPNAME) @@ -299,7 +326,10 @@ class PlateInitializer: raise RuntimeError("Experiment %s has no library template value in property %s" \ % (self.experimentId, self.LIBRARY_TEMPLATE_PROPNAME)) - tsvLists = [ line.split("\t") for line in template.splitlines() ] + lines = template.splitlines() + lines = removeTrailingEmptyElements(lines) + tsvLists = [ removeTrailingEmptyElements(line.split("\t")) for line in lines ] + self.validateLibraryDimensions(tsvLists) library = {} @@ -521,10 +551,10 @@ class MyImageDataSetConfig(SimpleImageDataConfig): if incoming.isDirectory(): transaction = service.transaction(incoming, factory) - (batchName, barCode) = parseIncomingDirname(incoming.getName()) - plate = findPlateByCode(barCode) + (batchName, plateCode) = parseIncomingDirname(incoming.getName()) + plate = findPlateByCode(plateCode) if not plate.getExperiment(): - raise RuntimeError("Plate with code '%(barCode)s' is not associated with experiment" % vars()) + raise RuntimeError("Plate with code '%(plateCode)s' is not associated with experiment" % vars()) experimentId = plate.getExperiment().getExperimentIdentifier() experiment = transaction.getExperiment(experimentId) diff --git a/sanofi/sourceTest/java/ch/systemsx/cisd/sanofi/dss/test/SanofiDropboxJythonTest.java b/sanofi/sourceTest/java/ch/systemsx/cisd/sanofi/dss/test/SanofiDropboxJythonTest.java index c324c81d458a3142d994f5455f5085de1c184bf5..939a1ad02c4d8f7fd158aaee40685bd1d4532826 100644 --- a/sanofi/sourceTest/java/ch/systemsx/cisd/sanofi/dss/test/SanofiDropboxJythonTest.java +++ b/sanofi/sourceTest/java/ch/systemsx/cisd/sanofi/dss/test/SanofiDropboxJythonTest.java @@ -17,6 +17,7 @@ package ch.systemsx.cisd.sanofi.dss.test; import static ch.systemsx.cisd.common.Constants.IS_FINISHED_PREFIX; +import static ch.systemsx.cisd.common.test.AssertionUtil.assertContains; import java.io.File; import java.io.IOException; @@ -126,37 +127,115 @@ public class SanofiDropboxJythonTest extends AbstractJythonDataSetHandlerTest private static final String EXPERIMENT_IDENTIFIER = "/SANOFI/PROJECT/EXP"; private static final String PLATE_IDENTIFIER = "/SANOFI/TEST-PLATE"; - @BeforeMethod + private RecordingMatcher<ch.systemsx.cisd.openbis.generic.shared.dto.AtomicEntityOperationDetails> atomicatOperationDetails; + + private RecordingMatcher<ListMaterialCriteria> materialCriteria; + + private RecordingMatcher<String> email; + @Override + @BeforeMethod public void setUp() throws IOException { super.setUp(); + atomicatOperationDetails = + new RecordingMatcher<ch.systemsx.cisd.openbis.generic.shared.dto.AtomicEntityOperationDetails>(); + materialCriteria = new RecordingMatcher<ListMaterialCriteria>(); + email = new RecordingMatcher<String>(); } @Test - public void testHappyCaseWithLibraryCreation() throws IOException + public void testLibraryWider() throws IOException { - setUpHomeDataBaseExpectations(); - Properties properties = - createThreadPropertiesRelativeToScriptsFolder("dropbox-all-in-one-with-library.py"); - createHandler(properties, false, true); - createData(); + createDataSetHandler(false, false); + final Sample plate = + plateWithLibTemplateAndGeometry("1.45\t\tH\n0.12\t0.002\tL", "10_WELLS_1X10"); + context.checking(new Expectations() + { + { - final String libraryTemplate = "1.45\t\tH\n0.12\t0.002\tL"; - final Sample plate = createPlate(libraryTemplate, "6_WELLS_2X3"); - setUpPlateSearchExpectations(plate); - setUpLibraryTemplateExpectations(plate); + SampleIdentifier sampleIdentifier = + SampleIdentifierFactory.parse(plate.getIdentifier()); + one(openBisService).tryGetSampleWithExperiment(sampleIdentifier); + will(returnValue(plate)); + + one(mailClient).sendMessage(with(any(String.class)), with(email), + with(aNull(String.class)), with(any(From.class)), + with(equal(EXPERIMENT_RECIPIENTS))); + } + }); + + try + { + handler.handle(markerFile); + fail("Registration should fail with library validation error"); + } catch (RuntimeException rex) + { + final String error = + "The property LIBRARY_TEMPLATE of experiment '/SANOFI/PROJECT/EXP' contains 2 rows, " + + "but the geometry of plate 'TEST-PLATE' allows a maximum of 1 rows. You should either reduce the " + + "number of rows in the library template or change the plate geometry."; + assertContains(error, rex.getMessage()); + assertContains(error, email.recordedObject()); + assertContains(IMAGE_DATA_SET_DIR_NAME, email.recordedObject()); + } + + context.assertIsSatisfied(); + } + + @Test + public void testLibraryHigher() throws IOException + { + createDataSetHandler(false, false); + final Sample plate = + plateWithLibTemplateAndGeometry("1.45\t\tH\n0.12\t0.002\tL", "5_WELLS_5X1"); + context.checking(new Expectations() + { + { + + SampleIdentifier sampleIdentifier = + SampleIdentifierFactory.parse(plate.getIdentifier()); + one(openBisService).tryGetSampleWithExperiment(sampleIdentifier); + will(returnValue(plate)); + + one(mailClient).sendMessage(with(any(String.class)), with(email), + with(aNull(String.class)), with(any(From.class)), + with(equal(EXPERIMENT_RECIPIENTS))); + } + }); + + try + { + handler.handle(markerFile); + fail("Registration should fail with library validation error"); + } catch (RuntimeException rex) + { + final String error = + "The property LIBRARY_TEMPLATE of experiment '/SANOFI/PROJECT/EXP' contains 3 " + + "columns in row 1, but the geometry of plate 'TEST-PLATE' allows a maximum of " + + "5 columns. You should either reduce the number of columns in the library " + + "template or change the plate geometry."; + assertContains(error, rex.getMessage()); + assertContains(error, email.recordedObject()); + assertContains(IMAGE_DATA_SET_DIR_NAME, email.recordedObject()); + } + + context.assertIsSatisfied(); + } + + @Test + public void testHappyCaseWithLibraryCreation() throws IOException + { + createDataSetHandler(false, true); + final Sample plate = + plateWithLibTemplateAndGeometry("1.45\t\tH\n0.12\t0.002\tL", "6_WELLS_10X10"); final MockDataSet<Map<String, Object>> queryResult = new MockDataSet<Map<String, Object>>(); queryResult.add(createQueryResult("A1")); queryResult.add(createQueryResult("B1")); queryResult.add(createQueryResult("B2")); - final RecordingMatcher<ch.systemsx.cisd.openbis.generic.shared.dto.AtomicEntityOperationDetails> atomicatOperationDetails = - new RecordingMatcher<ch.systemsx.cisd.openbis.generic.shared.dto.AtomicEntityOperationDetails>(); - final RecordingMatcher<ListMaterialCriteria> materialCriteria = - new RecordingMatcher<ListMaterialCriteria>(); - final RecordingMatcher<String> email = new RecordingMatcher<String>(); + setDataSetExpectations(); context.checking(new Expectations() { { @@ -171,30 +250,6 @@ public class SanofiDropboxJythonTest extends AbstractJythonDataSetHandlerTest exactly(5).of(openBisService).createPermId(); will(returnValue("well-permId")); - one(openBisService).createDataSetCode(); - will(returnValue(IMAGE_DATA_SET_CODE)); - - one(openBisService).createDataSetCode(); - will(returnValue(OVERLAY_DATA_SET_CODE)); - - one(openBisService).createDataSetCode(); - will(returnValue(ANALYSIS_DATA_SET_CODE)); - - one(dataSetValidator).assertValidDataSet( - IMAGE_DATA_SET_TYPE, - new File(new File(stagingDirectory, IMAGE_DATA_SET_CODE), - IMAGE_DATA_SET_DIR_NAME)); - - one(dataSetValidator).assertValidDataSet( - OVERLAY_DATA_SET_TYPE, - new File(new File(stagingDirectory, OVERLAY_DATA_SET_CODE), - OVERLAYS_DATA_SET_DIR_NAME)); - - one(dataSetValidator).assertValidDataSet( - ANALYSIS_DATA_SET_TYPE, - new File(new File(stagingDirectory, ANALYSIS_DATA_SET_CODE), - ANALYSIS_DATA_SET_FILE_NAME)); - SampleIdentifier sampleIdentifier = SampleIdentifierFactory.parse(plate.getIdentifier()); exactly(4).of(openBisService).tryGetSampleWithExperiment(sampleIdentifier); @@ -247,10 +302,11 @@ public class SanofiDropboxJythonTest extends AbstractJythonDataSetHandlerTest assertEquals(ANALYSIS_DATA_SET_CODE, analysisDataSet.getCode()); assertEquals(ANALYSIS_DATA_SET_TYPE, analysisDataSet.getDataSetType()); + AssertionUtil .assertContains( - "New data for the plate <a href='https://bwl27.sanofi-aventis.com:8443/openbis#entity=SAMPLE" - + "&sample_type=PLATE&action=SEARCH&code=TEST-PLATE'>TEST-PLATE</a> has been registered.", + "New data from folder 'batchNr_plateCode.variant_2011.07.05' has been successfully registered in plate " + + "<a href='https://bwl27.sanofi-aventis.com:8443/openbis#entity=SAMPLE&sample_type=PLATE&action=SEARCH&code=plateCode'>plateCode</a>", email.recordedObject()); context.assertIsSatisfied(); } @@ -331,6 +387,57 @@ public class SanofiDropboxJythonTest extends AbstractJythonDataSetHandlerTest } + public Sample plateWithLibTemplateAndGeometry(String libraryTemplate, String plateGeometry) + throws IOException + { + Sample plate = createPlate(libraryTemplate, plateGeometry); + setUpPlateSearchExpectations(plate); + setUpLibraryTemplateExpectations(plate); + return plate; + } + + private void createDataSetHandler(boolean shouldRegistrationFail, boolean rethrowExceptions) + throws IOException + { + setUpHomeDataBaseExpectations(); + Properties properties = + createThreadPropertiesRelativeToScriptsFolder("dropbox-all-in-one-with-library.py"); + createHandler(properties, shouldRegistrationFail, rethrowExceptions); + createData(); + } + + private void setDataSetExpectations() + { + context.checking(new Expectations() + { + { + one(openBisService).createDataSetCode(); + will(returnValue(IMAGE_DATA_SET_CODE)); + + one(openBisService).createDataSetCode(); + will(returnValue(OVERLAY_DATA_SET_CODE)); + + one(openBisService).createDataSetCode(); + will(returnValue(ANALYSIS_DATA_SET_CODE)); + + one(dataSetValidator).assertValidDataSet( + IMAGE_DATA_SET_TYPE, + new File(new File(stagingDirectory, IMAGE_DATA_SET_CODE), + IMAGE_DATA_SET_DIR_NAME)); + + one(dataSetValidator).assertValidDataSet( + OVERLAY_DATA_SET_TYPE, + new File(new File(stagingDirectory, OVERLAY_DATA_SET_CODE), + OVERLAYS_DATA_SET_DIR_NAME)); + + one(dataSetValidator).assertValidDataSet( + ANALYSIS_DATA_SET_TYPE, + new File(new File(stagingDirectory, ANALYSIS_DATA_SET_CODE), + ANALYSIS_DATA_SET_FILE_NAME)); + } + }); + } + private void setUpPlateSearchExpectations(final Sample plate) { context.checking(new Expectations()