View Javadoc

1   /*
2    * #%L
3    * JavaHg
4    * %%
5    * Copyright (C) 2011 aragost Trifork ag
6    * %%
7    * Permission is hereby granted, free of charge, to any person obtaining a copy
8    * of this software and associated documentation files (the "Software"), to deal
9    * in the Software without restriction, including without limitation the rights
10   * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11   * copies of the Software, and to permit persons to whom the Software is
12   * furnished to do so, subject to the following conditions:
13   * 
14   * The above copyright notice and this permission notice shall be included in
15   * all copies or substantial portions of the Software.
16   * 
17   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23   * THE SOFTWARE.
24   * #L%
25   */
26  package com.aragost.javahg.internals;
27  
28  import java.io.File;
29  import java.io.IOException;
30  import java.io.InputStream;
31  import java.lang.reflect.Field;
32  import java.util.Collections;
33  import java.util.List;
34  
35  import org.junit.Assert;
36  import org.junit.Ignore;
37  import org.junit.Test;
38  
39  import com.aragost.javahg.BaseRepository;
40  import com.aragost.javahg.Bundle;
41  import com.aragost.javahg.Changeset;
42  import com.aragost.javahg.Repository;
43  import com.aragost.javahg.RepositoryConfiguration;
44  import com.aragost.javahg.commands.AddCommand;
45  import com.aragost.javahg.commands.CommitCommand;
46  import com.aragost.javahg.commands.ExecutionException;
47  import com.aragost.javahg.commands.IncomingCommand;
48  import com.aragost.javahg.commands.LogCommand;
49  import com.aragost.javahg.commands.VersionCommand;
50  import com.aragost.javahg.test.AbstractTestCase;
51  import com.aragost.javahg.test.JavaHgTestExtension;
52  import com.google.common.base.Strings;
53  import com.google.common.collect.Lists;
54  import com.google.common.io.Files;
55  
56  public class ServerTest extends AbstractTestCase {
57  
58      private List<String> empty = Collections.emptyList();
59  
60      @Test
61      public void testStartStop() throws IOException {
62          File dir = Files.createTempDir();
63          Server server = new Server(RepositoryConfiguration.DEFAULT.getHgBin(), 
64                                     RepositoryConfiguration.DEFAULT.getEncoding());
65          server.initMecurialRepository(dir);
66          server.start(dir, null, empty, null, null);
67          server.stop();
68          deleteTempDir(dir);
69      }
70  
71      @Test
72      public void testStopWhileProducingOutput() throws IOException {
73          Repository repo = getTestRepository();
74          Server server = getFirstServer(repo);
75          InputStream stdout = server.runCommand(Lists.newArrayList("version"), VersionCommand.on(repo));
76  
77          // Nothing is yet read from stdout, stop the server. You
78          // should then get
79          // an IOException reading from stdout.
80          server.stop();
81          try {
82              stdout.read();
83              Assert.fail("IOException expected");
84          } catch (IOException e) {
85              // success
86          }
87  
88          server.start(repo.getDirectory(), null, empty, null, null);
89  
90          VersionCommand.on(repo).execute();
91      }
92  
93      @Test
94      public void testLock() throws IOException, InterruptedException {
95          Repository repo = getTestRepository();
96          TestableCommand command = new TestableCommand(repo, "version");
97  
98          // The command must produce output so that it wont finish
99          // before we empty the stdout. That way we can keep two
100         // commands running at the same time and trigger the
101         // IllegalStateException below .
102         InputStream stdout = command.executeToStream();
103         try {
104             Server server = getFirstServer(repo);
105             server.runCommand(Lists.newArrayList("version"), command);
106 
107             Assert.fail("Exception expected");
108         } catch (IllegalStateException e) {
109             Utils.consumeAll(stdout);
110         }
111 
112         command.executeToStream();
113     }
114 
115     @Test
116     public void testServerRefCount() throws IOException {
117         BaseRepository repo = getTestRepository();
118         BaseRepository repo2 = getTestRepository2();
119         writeFile("a");
120         commit();
121         Bundle bundle = IncomingCommand.on(repo2).execute(repo);
122         Repository repo3 = bundle.getOverlayRepository();
123         Assert.assertSame(repo2.getServerPool(), repo3.getServerPool());
124         ServerPool pool = repo2.getServerPool();
125         Assert.assertEquals(1, pool.getServers().size());
126         Assert.assertNotNull(pool.getServers().get(0).getProcess());
127         repo2.close();
128         Assert.assertNotNull(pool.getServers().get(0).getProcess());
129         Server server = pool.getServers().get(0);
130         bundle.close();
131         Assert.assertNull(server.getProcess());
132         Assert.assertTrue(pool.getServers().isEmpty());
133     }
134 
135     @Test
136     public void testConfigChanges() throws IOException {
137         BaseRepository repo = getTestRepository();
138         GenericCommand cmd = new GenericCommand(repo, "version") {
139             {
140                 {
141                     cmdAppend("--config", "ui.username=xxx");
142                 }
143             }
144         };
145         cmd.execute();
146         writeFile("A");
147         repo.workingCopy().add("A");
148         try {
149             // The previous config change is not forgotten
150             CommitCommand commit = CommitCommand.on(repo).message("m");
151             Changeset cs = commit.execute();
152             assertFailedExecution(commit, "Username is " + cs.getUser());
153         } catch (ExecutionException e) {
154             Assert.assertTrue(e.getMessage().startsWith("no username supplied "));
155         }
156     }
157 
158     @Test
159     public void testKillServerProcess() throws IOException {
160         File dir = Files.createTempDir();
161         RepositoryConfiguration repoConfig = new RepositoryConfiguration();
162         repoConfig.addExtension(JavaHgTestExtension.class);
163         Repository repo = Repository.create(repoConfig, dir);
164         Process process = getFirstServer(repo).getProcess();
165         process.destroy();
166         // Process is dead and we can't send command
167         try {
168             VersionCommand cmd = VersionCommand.on(repo);
169             cmd.execute();
170             assertFailedExecution(cmd);
171         } catch (UnexpectedServerTerminationException e) {
172             // success
173         }
174         repo.close();
175 
176         repo = Repository.open(repoConfig, dir);
177         String longStringThatDoesntFitInBuffers = Strings.repeat("x", 10 * 1000 * 1000);
178         GenericCommand cmd = new GenericCommand(repo, "javahg-write");
179         HgInputStream stream = cmd.launchStream("o", longStringThatDoesntFitInBuffers);
180         boolean killed = killProcess(process);
181         if (killed) {
182             // Command is now sent, but we can't read output
183             try {
184                 // String s =
185                 Utils.readStream(stream, repo.getServerPool().newDecoder());
186                 // TODO This doesn't work on Linux, why?
187                 // Assert.fail("Exception expected. Read " +
188                 // s.length() + " bytes");
189             } catch (UnexpectedServerTerminationException e) {
190                 System.err.println("Exit value in testKillServerProcess: " + e.getExitValue());
191                 // success
192             }
193         }
194         repo.close();
195 
196         deleteTempDir(dir);
197     }
198 
199     @Test
200     @Ignore
201     public void testStderrDuringStartup() throws IOException {
202         RepositoryConfiguration conf = new RepositoryConfiguration();
203         String stderr = retrieveStartupStderr(conf);
204         Assert.assertTrue("stderr=" + stderr,
205                 stderr.startsWith("*** failed to import extension javahgmissing from javahgmissing: [Errno 2]"));
206     }
207 
208     @Test
209     public void testStderrDuringStartupWillFullBuffer() throws IOException {
210         RepositoryConfiguration conf = new RepositoryConfiguration();
211         conf.setStderrBufferSize(1);
212         String stderr = retrieveStartupStderr(conf);
213         Assert.assertEquals("*", stderr);
214     }
215 
216     /** validate that clone requiring auth will use auth in hgrc */
217     @Test
218     public void testCloneRequiringAuth() throws Exception {
219         BaseRepository repoA = getTestRepository();
220 
221         writeFile(repoA, "x", "abc");
222         AddCommand.on(repoA).execute();
223         CommitCommand.on(repoA).message("added x").user("user").execute();
224 
225         // extension requires auth for requests via hg serve
226         String extConfig = "extensions.ra="
227                 + Utils.resourceAsFile("/require-auth.py").getPath();
228 
229         // repository accessed via http requires auth
230 
231         ServeState serveState = startServing(repoA, "--config", extConfig);
232 
233         try {
234             int port = serveState.getPort();
235 
236             // cloning with an hgrc with valid auth section should succeed
237             File cloneDir = Files.createTempDir();
238             Server server = new Server(
239                     RepositoryConfiguration.DEFAULT.getHgBin(),
240                     RepositoryConfiguration.DEFAULT.getEncoding());
241             server.cloneMercurialRepository(cloneDir,
242                     Utils.resourceAsFile("/test-hgrc-auth").getPath(),
243                     "http://localhost:" + port);
244             String xContents = Files.readFirstLine(new File(cloneDir, "x"),
245                     utf8());
246             Assert.assertEquals("abc", xContents);
247             deleteTempDir(cloneDir);
248 
249             // cloning with no hgrc should generate auth exception
250             File cloneDir2 = Files.createTempDir();
251             Server server2 = new Server(
252                     RepositoryConfiguration.DEFAULT.getHgBin(),
253                     RepositoryConfiguration.DEFAULT.getEncoding());
254             try {
255                 server2.cloneMercurialRepository(cloneDir, "",
256                         "http://localhost:" + port);
257                 Assert.fail("Didn't get expected http authorization exception");
258             } catch (Exception e) {
259                 if (!e.getMessage().contains("http authorization required")) {
260                     Assert.fail("Didn't get expected http authorization exception. Got "
261                             + e);
262                 }
263             }
264             deleteTempDir(cloneDir2);
265         } finally {
266             serveState.stop();
267         }
268     }
269 
270     /**
271      * Helper function for a couple of test cases.
272      * 
273      * @param conf
274      * @return
275      * @throws IOException
276      */
277     private String retrieveStartupStderr(RepositoryConfiguration conf) throws IOException {
278         conf.setHgrcPath(Utils.resourceAsFile("/missing-extension.hgrc").getPath());
279         File dir = Files.createTempDir();
280         BaseRepository repo = Repository.create(conf, dir);
281         String stderr = getFirstServer(repo).getStartupStderr();
282         repo.close();
283         deleteTempDir(dir);
284         return stderr;
285     }
286 
287     /**
288      * Kill the process with 'kill -9'. The 'kill -9' does not give
289      * the process a change to react to the kill signal. Using the
290      * {@link Process#destroy()} the process is performing cleanup
291      * <p>
292      * This method calls the 'kill' via system call, and it will for
293      * example not work on Windows. It is also using platform
294      * dependent reflection code to obtain the pid of the process.
295      * 
296      * @return true if 'kill' command was successful, false otherwise
297      * @param process
298      */
299     private boolean killProcess(Process process) {
300         int pid = readPid(process);
301         if (pid != 0) {
302             killProcess(pid);
303             return true;
304         }
305         return false;
306     }
307 
308     /**
309      * Attempt to read the pid for the process. If not possible return
310      * 0 otherwise return -1
311      * 
312      * @param process
313      * @return
314      */
315     private static int readPid(Process process) {
316         try {
317             Field pidField = process.getClass().getDeclaredField("pid");
318             pidField.setAccessible(true);
319             if (pidField.getType().equals(Integer.TYPE)) {
320                 String osName = System.getProperty("os.name");
321                 int pid = pidField.getInt(process);
322                 if (osName.startsWith("Mac OS X")) {
323                     // Hmm, strange but it actuall seems like the
324                     // value of
325                     // the pid is 1 less than the actual pid?!
326                     pid++;
327                 }
328                 return pid;
329             }
330         } catch (NoSuchFieldException e) {
331             return 0;
332         } catch (Exception e) {
333             throw Utils.asRuntime(e);
334         }
335         return 0;
336     }
337     
338     @Test
339     public void testServerIdle() throws InterruptedException {
340         RepositoryConfiguration conf = makeRepoConf();
341         conf.setServerIdleTime(1);
342         BaseRepository repo = Repository.create(conf, Files.createTempDir());
343 
344         Assert.assertEquals(1, repo.getServerPool().getNumIdleServers());
345 
346         Thread.sleep(2000);
347 
348         Assert.assertEquals(0, repo.getServerPool().getNumIdleServers());
349         LogCommand.on(repo).execute();
350         Assert.assertEquals(1, repo.getServerPool().getNumIdleServers());
351     }
352 }