/************************************************************************* * * * This file is part of the 20n/act project. * * 20n/act enables DNA prediction for synthetic biology/bioengineering. * * Copyright (C) 2017 20n Labs, Inc. * * * * Please direct all queries to act@20n.com. * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see <http://www.gnu.org/licenses/>. * * * *************************************************************************/ package com.act.biointerpretation.test.util; import act.server.MongoDB; import act.server.NoSQLAPI; import act.shared.Chemical; import act.shared.Organism; import act.shared.Reaction; import act.shared.Seq; import com.mongodb.DBObject; import org.json.JSONObject; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; public class MockedNoSQLAPI { public static final Answer CRASH_BY_DEFAULT = new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { throw new RuntimeException(String.format("Unexpected mock method called: %s", invocation.getMethod().getName())); } }; NoSQLAPI mockNoSQLAPI = null; MongoDB mockReadMongoDB = null; MongoDB mockWriteMongoDB = null; Map<Long, Reaction> idToReactionMap = new HashMap<>(); Map<Long, Chemical> idToChemicalMap = new HashMap<>(); List<Chemical> chemicals = new ArrayList<>(); List<Organism> orgs = new ArrayList<>(); final List<Reaction> writtenReactions = new ArrayList<>(); final Map<Long, Chemical> writtenChemicals = new HashMap<>(); final Map<Long, String> writtenOrganismNames = new HashMap<>(); final Map<Long, Seq> writtenSequences = new HashMap<>(); final Map<Long, Seq> seqMap = new HashMap<>(); final Map<Long, String> readOrganismNames = new HashMap<>(); public static Reaction copyReaction(Reaction r, Long newId) { Reaction newR = new Reaction(newId, r.getSubstrates(), r.getProducts(), r.getSubstrateCofactors(), r.getProductCofactors(), r.getCoenzymes(), r.getECNum(), r.getConversionDirection(), r.getPathwayStepDirection(), r.getReactionName(), r.getRxnDetailType()); for (JSONObject protein : r.getProteinData()) { JSONObject newProtein = new JSONObject(protein, JSONObject.getNames(protein)); newR.addProteinData(newProtein); } Long[] substrates = r.getSubstrates(); for (int i = 0; i < substrates.length; i++) { newR.setSubstrateCoefficient(substrates[i], r.getSubstrateCoefficient(substrates[i])); } Long[] products = r.getProducts(); for (int i = 0; i < products.length; i++) { newR.setProductCoefficient(products[i], r.getProductCoefficient(products[i])); } if (r.getMechanisticValidatorResult() != null) { newR.setMechanisticValidatorResult(r.getMechanisticValidatorResult()); } return newR; } public MockedNoSQLAPI() { } public void installMocks(List<Reaction> testReactions, List<Seq> sequences, Map<Long, String> orgNames, Map<Long, String> chemIdToInchi) { installMocks(testReactions, Collections.EMPTY_LIST, sequences, orgNames, chemIdToInchi); } public void installMocks(List<Reaction> testReactions, List<Long> testChemIds, List<Seq> sequences, Map<Long, String> orgNames, Map<Long, String> chemIdToInchi) { this.readOrganismNames.putAll(orgNames); for (Map.Entry<Long, String> orgName : readOrganismNames.entrySet()) { orgs.add(new Organism(orgName.getKey(), orgName.getValue())); } for (Seq seq : sequences) { seqMap.put(Long.valueOf(seq.getUUID()), seq); } // Mock the NoSQL API and its DB connections, throwing an exception if an unexpected method gets called. this.mockNoSQLAPI = mock(NoSQLAPI.class, CRASH_BY_DEFAULT); this.mockReadMongoDB = mock(MongoDB.class, CRASH_BY_DEFAULT); this.mockWriteMongoDB = mock(MongoDB.class, CRASH_BY_DEFAULT); /* Note: the Mockito .when(<method call>) API doesn't seem to work on the NoSQLAPI and MongoDB mocks instantiated * above. Specifying a mocked answer like: * Mockito.when(mockNoSQLAPI.getReadDB()).thenReturn(mockReadMongoDB); * invokes mockNoSQLAPI.getReadDB() (which is not unreasonable given the method call definition, which looks like * invocation) before its mocked behavior is defined. * * See https://groups.google.com/forum/#!topic/mockito/CqlI4EAvTwA for a thread on this issue. * * It's possible that specifying `CRASH_BY_DEFAULT` as the default action is interfering with Mockito's mechanism * for intercepting and overriding method calls. However, having the test crash when methods whose behavior * hasn't been explicitly re-defined or allowed to propagate to the normal method seems like an important safety * check. As such, we can work around the issue by using the `do*` form of mocking, where the stubbing API allows * us to specify the method whose behavior to intercept separately from its invocation. These calls look like: * Mockito.doAnswer(new Answer() { ... do some work ... }).when(mockObject).methodName(argMatchers) * which are a bit backwards but actually work in practice. * * Note also that we could potentially use spys instead of defining explicit mock behavior via Answers and * capturing arguments. That said, the Answer API is pretty straightforward to use and gives us a great deal of * flexibility when defining mock behavior. And since it works for now, we'll keep it until somebody writes * something better! */ doReturn(this.mockReadMongoDB).when(this.mockNoSQLAPI).getReadDB(); doReturn(this.mockWriteMongoDB).when(this.mockNoSQLAPI).getWriteDB(); for (Reaction r : testReactions) { this.idToReactionMap.put(Long.valueOf(r.getUUID()), r); Long[] substrates = r.getSubstrates(); Long[] products = r.getProducts(); List<Long> allSubstratesProducts = new ArrayList<>(substrates.length + products.length); allSubstratesProducts.addAll(Arrays.asList(substrates)); allSubstratesProducts.addAll(Arrays.asList(products)); for (Long id : allSubstratesProducts) { if (!this.idToChemicalMap.containsKey(id)) { Chemical c = new Chemical(id); if (chemIdToInchi.containsKey(id)) { c.setInchi(chemIdToInchi.get(id)); } else { // Use /FAKE/BRENDA prefix to avoid computing InChI keys. c.setInchi(String.format("InChI=/FAKE/BRENDA/TEST/%d", id)); } this.idToChemicalMap.put(id, c); } } } for (Long id : testChemIds) { if (!this.idToChemicalMap.containsKey(id)) { Chemical c = new Chemical(id); if (chemIdToInchi.containsKey(id)) { c.setInchi(chemIdToInchi.get(id)); } else { // Use /FAKE/BRENDA prefix to avoid computing InChI keys. c.setInchi(String.format("InChI=/FAKE/BRENDA/TEST/%d", id)); } this.idToChemicalMap.put(id, c); } } List<Long> chemicalIds = new ArrayList<>(this.idToChemicalMap.keySet()); Collections.sort(chemicalIds); /* **************************************** * Read DB and NoSQLAPI read method mocking */ // Return the set of artificial reactions we created when the caller asks for an iterator over the read DB. doReturn(testReactions.iterator()).when(mockNoSQLAPI).readRxnsFromInKnowledgeGraph(); doAnswer(new Answer<Iterator<Chemical>>() { @Override public Iterator<Chemical> answer(InvocationOnMock invocation) throws Throwable { return new Iterator<Chemical>() { Iterator<Long> idIterator = chemicalIds.iterator(); @Override public boolean hasNext() { return idIterator.hasNext(); } @Override public Chemical next() { return idToChemicalMap.get(idIterator.next()); } }; } }).when(mockNoSQLAPI).readChemsFromInKnowledgeGraph(); doReturn(sequences.iterator()).when(mockNoSQLAPI).readSeqsFromInKnowledgeGraph(); doReturn(orgs.iterator()).when(mockNoSQLAPI).readOrgsFromInKnowledgeGraph(); // Look up reactions/chems by id in the maps we just created. doAnswer(new Answer<Reaction>() { @Override public Reaction answer(InvocationOnMock invocation) throws Throwable { return idToReactionMap.get(invocation.getArgumentAt(0, Long.class)); } }).when(mockNoSQLAPI).readReactionFromInKnowledgeGraph(any(Long.class)); doAnswer(new Answer<Chemical>() { @Override public Chemical answer(InvocationOnMock invocation) throws Throwable { return idToChemicalMap.get(invocation.getArgumentAt(0, Long.class)); } }).when(mockNoSQLAPI).readChemicalFromInKnowledgeGraph(any(Long.class)); doAnswer(new Answer<String>() { @Override public String answer(InvocationOnMock invocation) throws Throwable { return readOrganismNames.get(invocation.getArgumentAt(0, Long.class)); } }).when(mockReadMongoDB).getOrganismNameFromId(any(Long.class)); doAnswer(new Answer<Seq>() { @Override public Seq answer(InvocationOnMock invocation) throws Throwable { Long id = invocation.getArgumentAt(0, Long.class); return seqMap.get(id); } }).when(mockReadMongoDB).getSeqFromID(any(Long.class)); doAnswer(new Answer<Long>() { @Override public Long answer(InvocationOnMock invocation) throws Throwable { String targetOrganism = invocation.getArgumentAt(0, String.class); for (Map.Entry<Long, String> entry : readOrganismNames.entrySet()) { if (entry.getValue().equals(targetOrganism)) { return entry.getKey(); } } return null; } }).when(mockReadMongoDB).getOrganismId(any(String.class)); /* **************************************** * Write DB and NoSQLAPI write method mocking */ // Capture written reactions, making a copy with a fresh ID for later verification. doAnswer(new Answer<Integer>() { @Override public Integer answer(InvocationOnMock invocation) throws Throwable { Reaction r = invocation.getArgumentAt(0, Reaction.class); Long id = writtenReactions.size() + 1L; Reaction newR = copyReaction(r, id); writtenReactions.add(newR); return id.intValue(); } }).when(mockNoSQLAPI).writeToOutKnowlegeGraph(any(Reaction.class)); // See http://site.mockito.org/mockito/docs/current/org/mockito/Mockito.html#do_family_methods_stubs doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { Reaction toBeUpdated = invocation.getArgumentAt(0, Reaction.class); int id = invocation.getArgumentAt(1, Integer.class); int matchingIndex = -1; for (int i = 0; i < writtenReactions.size(); i++) { if (writtenReactions.get(i).getUUID() == id) { matchingIndex = i; break; } } if (matchingIndex == -1) { return null; } Reaction newR = copyReaction(toBeUpdated, Long.valueOf(id)); writtenReactions.set(matchingIndex, newR); return null; } }).when(mockWriteMongoDB).updateActReaction(any(Reaction.class), anyInt()); doAnswer(new Answer<Long>() { @Override public Long answer(InvocationOnMock invocation) throws Throwable { Chemical chem = invocation.getArgumentAt(0, Chemical.class); Long id = writtenChemicals.size() + 1L; Chemical newChem = new Chemical(id); newChem.setInchi(chem.getInChI()); writtenChemicals.put(id, newChem); return id; } }).when(mockNoSQLAPI).writeToOutKnowlegeGraph(any(Chemical.class)); doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { Organism org = invocation.getArgumentAt(0, Organism.class); // IDs aren't incrementally or randomly generated--make sure we reuse the old ones. writtenOrganismNames.put(org.getUUID(), org.getName()); return null; } }).when(mockWriteMongoDB).submitToActOrganismNameDB(any(Organism.class)); doAnswer(new Answer() { @Override public Long answer(InvocationOnMock invocation) throws Throwable { Long id = writtenOrganismNames.size() + MongoDB.ORG_ID_BASE; while (writtenOrganismNames.containsKey(id)) { id += 1L; // Avoid collisions as best we can. Or maybe this is a terrible idea. } writtenOrganismNames.put(id, invocation.getArgumentAt(0, String.class)); return id; } }).when(mockWriteMongoDB).submitToActOrganismNameDB(any(String.class)); doAnswer(new Answer<Long>() { @Override public Long answer(InvocationOnMock invocation) throws Throwable { String targetOrganism = invocation.getArgumentAt(0, String.class); for (Map.Entry<Long, String> entry : writtenOrganismNames.entrySet()) { if (entry.getValue().equals(targetOrganism)) { return entry.getKey(); } } return -1L; } }).when(mockWriteMongoDB).getOrganismId(any(String.class)); // TODO: there must be a better way than this, right? doAnswer(new Answer<Integer>() { @Override public Integer answer(InvocationOnMock invocation) throws Throwable { Long id = writtenSequences.size() + 1L; Seq.AccDB src = invocation.getArgumentAt(0, Seq.AccDB.class); String ec = invocation.getArgumentAt(1, String.class); String org = invocation.getArgumentAt(2, String.class); Long org_id = invocation.getArgumentAt(3, Long.class); String seq = invocation.getArgumentAt(4, String.class); List<JSONObject> pmids = invocation.getArgumentAt(5, List.class); Set<Long> rxns = invocation.getArgumentAt(6, Set.class); DBObject meta = invocation.getArgumentAt(7, DBObject.class); writtenSequences.put(id, Seq.rawInit(id, ec, org_id, org, seq, pmids, meta, src, rxns)); return id.intValue(); } }).when(mockWriteMongoDB).submitToActSeqDB( any(Seq.AccDB.class), any(String.class), any(String.class), any(Long.class), any(String.class), any(List.class), any(Set.class), any(DBObject.class) ); doAnswer(new Answer<Seq>() { @Override public Seq answer(InvocationOnMock invocation) throws Throwable { return writtenSequences.get(invocation.getArgumentAt(0, Long.class)); } }).when(mockWriteMongoDB).getSeqFromID(any(Long.class)); doAnswer(new Answer<Iterator<Seq>>() { @Override public Iterator<Seq> answer(InvocationOnMock invocation) throws Throwable { return writtenSequences.values().iterator(); } }).when(mockWriteMongoDB).getSeqIterator(); doAnswer(new Answer<Reaction>() { @Override public Reaction answer(InvocationOnMock invocation) throws Throwable { Long id = invocation.getArgumentAt(0, Long.class); for (int i = 0; i < writtenReactions.size(); i++) { if (writtenReactions.get(i).getUUID() == id) { return writtenReactions.get(i); } } return null; } }).when(mockWriteMongoDB).getReactionFromUUID(any(Long.class)); doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { Seq seq = invocation.getArgumentAt(0, Seq.class); if (writtenSequences.containsKey((long) seq.getUUID())) { Seq seqToUpdate = writtenSequences.get((long) seq.getUUID()); seqToUpdate.setReactionsCatalyzed(seq.getReactionsCatalyzed()); } return null; } }).when(mockWriteMongoDB).updateRxnRefs(any(Seq.class)); } public NoSQLAPI getMockNoSQLAPI() { return mockNoSQLAPI; } public MongoDB getMockReadMongoDB() { return mockReadMongoDB; } public MongoDB getMockWriteMongoDB() { return mockWriteMongoDB; } public Map<Long, Reaction> getIdToReactionMap() { return idToReactionMap; } public Map<Long, Chemical> getIdToChemicalMap() { return idToChemicalMap; } public List<Reaction> getWrittenReactions() { return writtenReactions; } public Map<Long, Chemical> getWrittenChemicals() { return writtenChemicals; } public Map<Long, String> getWrittenOrganismNames() { return writtenOrganismNames; } public Map<Long, Seq> getWrittenSequences() { return writtenSequences; } private Set<String> chemMapToInchiSet(Long[] ids, Map<Long, Chemical> chemMap) { Set<String> inchis = new HashSet<>(); for (Long id : ids) { Chemical c = chemMap.get(id); // Let NPEs happen here if bad ids are passed. inchis.add(c.getInChI()); } return inchis; } public Set<String> readDBChemicalIdsToInchis(Long[] ids) { return this.chemMapToInchiSet(ids, this.idToChemicalMap); } public Set<String> writeDBChemicalIdsToInchis(Long[] ids) { return this.chemMapToInchiSet(ids, this.writtenChemicals); } }