From 9936fbe3c96c24ba69f5376d562057f18605ff66 Mon Sep 17 00:00:00 2001 From: Winston Li Date: Wed, 24 Aug 2016 18:04:01 +0100 Subject: [PATCH] Implement and test the swap job, and add integration test --- .../wlgitbridge/application/GitBridgeApp.java | 2 +- .../application/config/Config.java | 8 ++ .../ic/wlgitbridge/bridge/repo/RepoStore.java | 5 +- .../bridge/swap/job/SwapJobConfig.java | 3 - .../bridge/swap/store/InMemorySwapStore.java | 4 + .../bridge/swap/store/SwapStore.java | 1 + .../git/handler/WLReceivePackFactory.java | 8 +- .../git/handler/hook/WriteLatexPutHook.java | 8 +- .../wlgitbridge/server/GitBridgeServer.java | 11 +- .../wlgitbridge/server/PostbackContents.java | 10 +- .../wlgitbridge/server/PostbackHandler.java | 8 +- .../WLGitBridgeIntegrationTest.java | 114 +++++++++++++++--- .../ac/ic/wlgitbridge/bridge/BridgeTest.java | 27 +++++ .../wlgbCanSwapProjects/state/state.json | 90 ++++++++++++++ .../state/testproj1/foo/bar/test.tex | 1 + .../state/testproj1/main.tex | 1 + .../state/testproj1/overleaf-white-410.png | Bin 0 -> 8786 bytes ...sions-a7e4de19d015c3e7477e3f7eaa6c418e.png | Bin 0 -> 3573 bytes .../state/testproj2/foo/bar/test.tex | 1 + .../state/testproj2/main.tex | 1 + 20 files changed, 258 insertions(+), 45 deletions(-) rename services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/{ => application}/WLGitBridgeIntegrationTest.java (90%) create mode 100644 services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/wlgbCanSwapProjects/state/state.json create mode 100644 services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/wlgbCanSwapProjects/state/testproj1/foo/bar/test.tex create mode 100644 services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/wlgbCanSwapProjects/state/testproj1/main.tex create mode 100644 services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/wlgbCanSwapProjects/state/testproj1/overleaf-white-410.png create mode 100644 services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/wlgbCanSwapProjects/state/testproj2/editor-versions-a7e4de19d015c3e7477e3f7eaa6c418e.png create mode 100644 services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/wlgbCanSwapProjects/state/testproj2/foo/bar/test.tex create mode 100644 services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/wlgbCanSwapProjects/state/testproj2/main.tex diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/application/GitBridgeApp.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/application/GitBridgeApp.java index 6e3f049cf6..5c909c3da3 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/application/GitBridgeApp.java +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/application/GitBridgeApp.java @@ -25,7 +25,7 @@ public class GitBridgeApp implements Runnable { "usage: writelatex-git-bridge config_file"; private String configFilePath; - private Config config; + Config config; private GitBridgeServer server; /** diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/application/config/Config.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/application/config/Config.java index 26ffa495fd..eb64fe15be 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/application/config/Config.java +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/application/config/Config.java @@ -98,6 +98,14 @@ public class Config implements JSONSource { postbackURL += "/"; } oauth2 = new Gson().fromJson(configObject.get("oauth2"), Oauth2.class); + swapStore = new Gson().fromJson( + configObject.get("swapStore"), + SwapStoreConfig.class + ); + swapJob = new Gson().fromJson( + configObject.get("swapJob"), + SwapJobConfig.class + ); } public String getSanitisedString() { diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/repo/RepoStore.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/repo/RepoStore.java index ed328079a4..bdb95ac828 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/repo/RepoStore.java +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/repo/RepoStore.java @@ -10,6 +10,9 @@ import java.util.Collection; */ public interface RepoStore { + /* Still need to get rid of these two methods. + Main dependency: GitRepoStore needs a Repository which needs a directory. + Instead, use a visitor or something. */ String getRepoStorePath(); File getRootDirectory(); @@ -20,7 +23,7 @@ public interface RepoStore { long totalSize(); - /* + /** * Tars and bzip2s the .git directory of the given project. Throws an * IOException if the project doesn't exist. The returned stream is a copy * of the original .git directory, which must be deleted using remove(). diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/swap/job/SwapJobConfig.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/swap/job/SwapJobConfig.java index dfacc0001e..057cb8148e 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/swap/job/SwapJobConfig.java +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/swap/job/SwapJobConfig.java @@ -5,9 +5,6 @@ package uk.ac.ic.wlgitbridge.bridge.swap.job; */ public class SwapJobConfig { - public static final SwapJobConfig DEFAULT = - new SwapJobConfig(1, 1, 2, 3600000); - private final int minProjects; private final int lowGiB; private final int highGiB; diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/swap/store/InMemorySwapStore.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/swap/store/InMemorySwapStore.java index d4c5a11136..a507e7e829 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/swap/store/InMemorySwapStore.java +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/swap/store/InMemorySwapStore.java @@ -19,6 +19,10 @@ public class InMemorySwapStore implements SwapStore { store = new HashMap<>(); } + public InMemorySwapStore(SwapStoreConfig __) { + this(); + } + @Override public void upload( String projectName, diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/swap/store/SwapStore.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/swap/store/SwapStore.java index 1f582230bb..168e959313 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/swap/store/SwapStore.java +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/swap/store/SwapStore.java @@ -17,6 +17,7 @@ public interface SwapStore { { put("noop", NoopSwapStore::new); + put("memory", InMemorySwapStore::new); put("s3", S3SwapStore::new); } diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/git/handler/WLReceivePackFactory.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/git/handler/WLReceivePackFactory.java index 542963767d..5ca19ba7d5 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/git/handler/WLReceivePackFactory.java +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/git/handler/WLReceivePackFactory.java @@ -19,10 +19,10 @@ import javax.servlet.http.HttpServletRequest; /* */ public class WLReceivePackFactory implements ReceivePackFactory { - private final Bridge bridgeAPI; + private final Bridge bridge; - public WLReceivePackFactory(Bridge bridgeAPI) { - this.bridgeAPI = bridgeAPI; + public WLReceivePackFactory(Bridge bridge) { + this.bridge = bridge; } @Override @@ -33,7 +33,7 @@ public class WLReceivePackFactory implements ReceivePackFactory() {{ put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/canServePushedFiles/state/state.json")).build()); }}); + put("wlgbCanSwapProjects", new HashMap() {{ + put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/wlgbCanSwapProjects/state/state.json")).build()); + }}); }}; @Rule @@ -587,6 +591,38 @@ public class WLGitBridgeIntegrationTest { wlgb.stop(); } + @Test + public void wlgbCanSwapProjects( + ) throws IOException, GitAPIException, InterruptedException { + MockSnapshotServer server = new MockSnapshotServer( + 3874, + getResource("/wlgbCanSwapProjects").toFile() + ); + server.start(); + server.setState(states.get("wlgbCanSwapProjects").get("state")); + GitBridgeApp wlgb = new GitBridgeApp(new String[] { + makeConfigFile(33874, 3874, new SwapJobConfig(1, 0, 0, 250)) + }); + wlgb.run(); + File dir = folder.newFolder(); + File rootGitDir = new File(wlgb.config.getRootGitDirectory()); + File testProj1ServerDir = new File(rootGitDir, "testproj1"); + File testProj2ServerDir = new File(rootGitDir, "testproj2"); + File testProj1Dir = cloneRepository("testproj1", 33874, dir); + assertTrue(testProj1ServerDir.exists()); + assertFalse(testProj2ServerDir.exists()); + cloneRepository("testproj2", 33874, dir); + while (testProj1ServerDir.exists()); + assertFalse(testProj1ServerDir.exists()); + assertTrue(testProj2ServerDir.exists()); + FileUtils.deleteDirectory(testProj1Dir); + cloneRepository("testproj1", 33874, dir); + while (testProj2ServerDir.exists()); + assertTrue(testProj1ServerDir.exists()); + assertFalse(testProj2ServerDir.exists()); + wlgb.stop(); + } + private File cloneRepository(String repositoryName, int port, File dir) throws IOException, InterruptedException { String repo = "git clone http://127.0.0.1:" + port + "/" + repositoryName + ".git"; Process gitProcess = runtime.exec(repo, null, dir); @@ -606,30 +642,72 @@ public class WLGitBridgeIntegrationTest { return repositoryDir; } - private String makeConfigFile(int port, int apiPort) throws IOException { + private String makeConfigFile( + int port, + int apiPort + ) throws IOException { + return makeConfigFile(port, apiPort, null); + } + + private String makeConfigFile( + int port, + int apiPort, + SwapJobConfig swapCfg + ) throws IOException { File wlgb = folder.newFolder(); File config = folder.newFile(); PrintWriter writer = new PrintWriter(config); - writer.println("{\n" + - "\t\"port\": " + port + ",\n" + - "\t\"rootGitDirectory\": \"" + wlgb.getAbsolutePath() + "\",\n" + - "\t\"apiBaseUrl\": \"http://127.0.0.1:" + apiPort + "/api/v0\",\n" + - "\t\"username\": \"\",\n" + - "\t\"password\": \"\",\n" + - "\t\"postbackBaseUrl\": \"http://127.0.0.1:" + port + "\",\n" + - "\t\"serviceName\": \"Overleaf\"\n," + - " \"oauth2\": {\n" + - " \"oauth2ClientID\": \"clientID\",\n" + - " \"oauth2ClientSecret\": \"oauth2 client secret\",\n" + - " \"oauth2Server\": \"https://www.overleaf.com\"\n" + - " }\n" + - "}\n"); + String cfgStr = + "{\n" + + " \"port\": " + port + ",\n" + + " \"rootGitDirectory\": \"" + + wlgb.getAbsolutePath() + + "\",\n" + + " \"apiBaseUrl\": \"http://127.0.0.1:" + + apiPort + + "/api/v0\",\n" + + " \"username\": \"\",\n" + + " \"password\": \"\",\n" + + " \"postbackBaseUrl\": \"http://127.0.0.1:" + + port + + "\",\n" + + " \"serviceName\": \"Overleaf\",\n" + + " \"oauth2\": {\n" + + " \"oauth2ClientID\": \"clientID\",\n" + + " \"oauth2ClientSecret\": \"oauth2 client secret\",\n" + + " \"oauth2Server\": \"https://www.overleaf.com\"\n" + + " }"; + if (swapCfg != null) { + cfgStr += ",\n" + + " \"swapStore\": {\n" + + " \"type\": \"memory\"\n" + + " },\n" + + " \"swapJob\": {\n" + + " \"minProjects\": " + + swapCfg.getMinProjects() + + ",\n" + + " \"lowGiB\": " + + swapCfg.getLowGiB() + + ",\n" + + " \"highGiB\": " + + swapCfg.getHighGiB() + + ",\n" + + " \"intervalMillis\": " + + swapCfg.getIntervalMillis() + + "\n" + + " }\n"; + } + cfgStr += "}\n"; + writer.print(cfgStr); writer.close(); return config.getAbsolutePath(); } private Path getResource(String path) { - return Paths.get("src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest" + path); + return Paths.get( + "src/test/resources/" + + "uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest" + path + ); } private InputStream getResourceAsStream(String path) { diff --git a/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/bridge/BridgeTest.java b/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/bridge/BridgeTest.java index ebe3b31fc0..a2ecda2850 100644 --- a/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/bridge/BridgeTest.java +++ b/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/bridge/BridgeTest.java @@ -3,15 +3,25 @@ package uk.ac.ic.wlgitbridge.bridge; import org.junit.Before; import org.junit.Test; import uk.ac.ic.wlgitbridge.bridge.db.DBStore; +import uk.ac.ic.wlgitbridge.bridge.db.ProjectState; import uk.ac.ic.wlgitbridge.bridge.lock.ProjectLock; import uk.ac.ic.wlgitbridge.bridge.repo.RepoStore; import uk.ac.ic.wlgitbridge.bridge.resource.ResourceCache; import uk.ac.ic.wlgitbridge.bridge.snapshot.SnapshotAPI; import uk.ac.ic.wlgitbridge.bridge.swap.job.SwapJob; import uk.ac.ic.wlgitbridge.bridge.swap.store.SwapStore; +import uk.ac.ic.wlgitbridge.data.model.Snapshot; +import uk.ac.ic.wlgitbridge.git.exception.GitUserException; +import java.io.IOException; +import java.util.ArrayDeque; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * Created by winston on 20/08/2016. @@ -56,4 +66,21 @@ public class BridgeTest { verify(swapJob).stop(); } + @Test + public void updatingRepositorySetsLastAccessedTime( + ) throws IOException, GitUserException { + ProjectRepo repo = mock(ProjectRepo.class); + when(repo.getProjectName()).thenReturn("asdf"); + when(dbStore.getProjectState("asdf")).thenReturn(ProjectState.PRESENT); + when( + snapshotAPI.getSnapshotsForProjectAfterVersion( + any(), + any(), + anyInt() + ) + ).thenReturn(new ArrayDeque()); + bridge.updateRepository(null, repo); + verify(dbStore).setLastAccessedTime(eq("asdf"), any()); + } + } \ No newline at end of file diff --git a/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/wlgbCanSwapProjects/state/state.json b/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/wlgbCanSwapProjects/state/state.json new file mode 100644 index 0000000000..77d610d756 --- /dev/null +++ b/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/wlgbCanSwapProjects/state/state.json @@ -0,0 +1,90 @@ +[ + { + "project": "testproj1", + "getDoc": { + "versionID": 1, + "createdAt": "2014-11-30T18:40:58.123Z", + "email": "jdleesmiller+1@gmail.com", + "name": "John+1" + }, + "getSavedVers": [ + { + "versionID": 1, + "comment": "added more info on doc GET and error details", + "email": "jdleesmiller+1@gmail.com", + "name": "John+1", + "createdAt": "2014-11-30T18:47:01.456Z" + } + ], + "getForVers": [ + { + "versionID": 1, + "srcs": [ + { + "content": "content\n", + "path": "main.tex" + }, + { + "content": "This text is from another file.", + "path": "foo/bar/test.tex" + } + ], + "atts": [ + { + "url": "http://127.0.0.1:3874/state/testproj1/overleaf-white-410.png", + "path": "overleaf-white-410.png" + } + ] + } + ], + "push": "success", + "postback": { + "type": "success", + "versionID": 2 + } + }, + { + "project": "testproj2", + "getDoc": { + "versionID": 1, + "createdAt": "2014-11-30T18:40:58.123Z", + "email": "jdleesmiller+1@gmail.com", + "name": "John+1" + }, + "getSavedVers": [ + { + "versionID": 1, + "comment": "added more info on doc GET and error details", + "email": "jdleesmiller+1@gmail.com", + "name": "John+1", + "createdAt": "2014-11-30T18:47:01.456Z" + } + ], + "getForVers": [ + { + "versionID": 1, + "srcs": [ + { + "content": "different content\n", + "path": "main.tex" + }, + { + "content": "a different one", + "path": "foo/bar/test.tex" + } + ], + "atts": [ + { + "url": "http://127.0.0.1:3874/state/testproj2/editor-versions-a7e4de19d015c3e7477e3f7eaa6c418e.png", + "path": "editor-versions-a7e4de19d015c3e7477e3f7eaa6c418e.png" + } + ] + } + ], + "push": "success", + "postback": { + "type": "success", + "versionID": 2 + } + } +] \ No newline at end of file diff --git a/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/wlgbCanSwapProjects/state/testproj1/foo/bar/test.tex b/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/wlgbCanSwapProjects/state/testproj1/foo/bar/test.tex new file mode 100644 index 0000000000..046794f19a --- /dev/null +++ b/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/wlgbCanSwapProjects/state/testproj1/foo/bar/test.tex @@ -0,0 +1 @@ +This text is from another file. \ No newline at end of file diff --git a/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/wlgbCanSwapProjects/state/testproj1/main.tex b/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/wlgbCanSwapProjects/state/testproj1/main.tex new file mode 100644 index 0000000000..d95f3ad14d --- /dev/null +++ b/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/wlgbCanSwapProjects/state/testproj1/main.tex @@ -0,0 +1 @@ +content diff --git a/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/wlgbCanSwapProjects/state/testproj1/overleaf-white-410.png b/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/wlgbCanSwapProjects/state/testproj1/overleaf-white-410.png new file mode 100644 index 0000000000000000000000000000000000000000..6a23d10c15d3eb7fcb18609be0bc4bf0706eca49 GIT binary patch literal 8786 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 literal 0 HcmV?d00001 diff --git a/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/wlgbCanSwapProjects/state/testproj2/editor-versions-a7e4de19d015c3e7477e3f7eaa6c418e.png b/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/wlgbCanSwapProjects/state/testproj2/editor-versions-a7e4de19d015c3e7477e3f7eaa6c418e.png new file mode 100644 index 0000000000000000000000000000000000000000..7fa339be7a9ab251a9d2adde54b39501bc0dc056 GIT binary patch literal 3573 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