Tests for memory leaks
authorisaacs <i@izs.me>
Thu, 3 May 2012 17:16:25 +0000 (10:16 -0700)
committerisaacs <i@izs.me>
Thu, 3 May 2012 17:36:17 +0000 (10:36 -0700)
.gitignore
Makefile
test/gc/test-http-client-connaborted.js [new file with mode: 0644]
test/gc/test-http-client-onerror.js [new file with mode: 0644]
test/gc/test-http-client-timeout.js [new file with mode: 0644]
test/gc/test-http-client.js [new file with mode: 0644]
test/gc/test-net-timeout.js [new file with mode: 0644]
test/gc/testcfg.py [new file with mode: 0644]
tools/test.py

index 3b2ad3f..937bfb3 100644 (file)
@@ -40,3 +40,4 @@ ipch/
 email.md
 blog.html
 deps/v8-*
+node_modules
index 0eb4605..5bd675c 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -32,7 +32,7 @@ install:
 uninstall:
        @$(WAF) uninstall
 
-test: all
+test: all node_modules/weak
        $(PYTHON) tools/test.py --mode=release simple message
 
 test-http1: all
@@ -41,7 +41,15 @@ test-http1: all
 test-valgrind: all
        $(PYTHON) tools/test.py --mode=release --valgrind simple message
 
-test-all: all
+node_modules/weak:
+       @if [ ! -f node ]; then make all; fi
+       @if [ ! -d node_modules ]; then mkdir -p node_modules; fi
+       ./node deps/npm/bin/npm-cli.js install weak --prefix="$(shell pwd)"
+
+test-gc: all node_modules/weak
+       $(PYTHON) tools/test.py --mode=release gc
+
+test-all: all node_modules/weak
        $(PYTHON) tools/test.py --mode=debug,release
        make test-npm
 
@@ -153,6 +161,7 @@ clean:
        $(WAF) clean
        -find tools -name "*.pyc" | xargs rm -f
        -rm -rf blog.html email.md
+       -rm -rf node_modules
 
 distclean: docclean
        -find tools -name "*.pyc" | xargs rm -f
diff --git a/test/gc/test-http-client-connaborted.js b/test/gc/test-http-client-connaborted.js
new file mode 100644 (file)
index 0000000..84d7ca5
--- /dev/null
@@ -0,0 +1,61 @@
+// just like test/gc/http-client.js,
+// but aborting every connection that comes in.
+
+function serverHandler(req, res) {
+  res.connection.destroy();
+}
+
+var http  = require('http'),
+    weak    = require('weak'),
+    done    = 0,
+    count   = 0,
+    countGC = 0,
+    todo    = 18,
+    common = require('../common.js'),
+    assert = require('assert'),
+    PORT = common.PORT;
+
+console.log('We should do '+ todo +' requests');
+
+var http = require('http');
+var server = http.createServer(serverHandler);
+server.listen(PORT, getall);
+
+function getall() {
+  for (var i = 0; i < todo; i++) {
+    (function(){
+      function cb(res) {
+        done+=1;
+        statusLater();
+      }
+
+      var req = http.get({
+        hostname: 'localhost',
+        pathname: '/',
+        port: PORT
+      }, cb).on('error', cb);
+
+      count++;
+      weak(req, afterGC);
+    })()
+  }
+}
+
+function afterGC(){
+  countGC ++;
+}
+
+function statusLater() {
+  setTimeout(status, 1);
+}
+
+function status() {
+  gc();
+  console.log('Done: %d/%d', done, todo);
+  console.log('Collected: %d/%d', countGC, count);
+  if (done === todo) {
+    console.log('All should be collected now.');
+    assert(count === countGC);
+    process.exit(0);
+  }
+}
diff --git a/test/gc/test-http-client-onerror.js b/test/gc/test-http-client-onerror.js
new file mode 100644 (file)
index 0000000..5872489
--- /dev/null
@@ -0,0 +1,66 @@
+// just like test/gc/http-client.js,
+// but with an on('error') handler that does nothing.
+
+function serverHandler(req, res) {
+  res.writeHead(200, {'Content-Type': 'text/plain'});
+  res.end('Hello World\n');
+}
+
+var http  = require('http'),
+    weak    = require('weak'),
+    done    = 0,
+    count   = 0,
+    countGC = 0,
+    todo    = 18,
+    common = require('../common.js'),
+    assert = require('assert'),
+    PORT = common.PORT;
+
+console.log('We should do '+ todo +' requests');
+
+var http = require('http');
+var server = http.createServer(serverHandler);
+server.listen(PORT, getall);
+
+function getall() {
+  for (var i = 0; i < todo; i++) {
+    (function(){
+      function cb(res) {
+        done+=1;
+        statusLater();
+      }
+      function onerror(er) {
+        throw er;
+      }
+
+      var req = http.get({
+        hostname: 'localhost',
+        pathname: '/',
+        port: PORT
+      }, cb).on('error', onerror);
+
+      count++;
+      weak(req, afterGC);
+    })()
+  }
+}
+
+function afterGC(){
+  countGC ++;
+}
+
+function statusLater() {
+  setTimeout(status, 1);
+}
+
+function status() {
+  gc();
+  console.log('Done: %d/%d', done, todo);
+  console.log('Collected: %d/%d', countGC, count);
+  if (done === todo) {
+    console.log('All should be collected now.');
+    assert(count === countGC);
+    process.exit(0);
+  }
+}
+
diff --git a/test/gc/test-http-client-timeout.js b/test/gc/test-http-client-timeout.js
new file mode 100644 (file)
index 0000000..32302a4
--- /dev/null
@@ -0,0 +1,69 @@
+// just like test/gc/http-client.js,
+// but with a timeout set
+
+function serverHandler(req, res) {
+  setTimeout(function () {
+    res.writeHead(200)
+    res.end('hello\n');
+  }, 100);
+}
+
+var http  = require('http'),
+    weak    = require('weak'),
+    done    = 0,
+    count   = 0,
+    countGC = 0,
+    todo    = 18,
+    common = require('../common.js'),
+    assert = require('assert'),
+    PORT = common.PORT;
+
+console.log('We should do '+ todo +' requests');
+
+var http = require('http');
+var server = http.createServer(serverHandler);
+server.listen(PORT, getall);
+
+function getall() {
+  for (var i = 0; i < todo; i++) {
+    (function(){
+      function cb() {
+        done+=1;
+        statusLater();
+      }
+
+      var req = http.get({
+        hostname: 'localhost',
+        pathname: '/',
+        port: PORT
+      }, cb);
+      req.on('error', cb);
+      req.setTimeout(10, function(){
+        console.log('timeout (expected)')
+      });
+
+      count++;
+      weak(req, afterGC);
+    })()
+  }
+}
+
+function afterGC(){
+  countGC ++;
+}
+
+function statusLater() {
+  setTimeout(status, 1);
+}
+
+function status() {
+  gc();
+  console.log('Done: %d/%d', done, todo);
+  console.log('Collected: %d/%d', countGC, count);
+  if (done === todo) {
+    console.log('All should be collected now.');
+    assert(count === countGC);
+    process.exit(0);
+  }
+}
+
diff --git a/test/gc/test-http-client.js b/test/gc/test-http-client.js
new file mode 100644 (file)
index 0000000..913ca7b
--- /dev/null
@@ -0,0 +1,63 @@
+// just a simple http server and client.
+
+function serverHandler(req, res) {
+  res.writeHead(200, {'Content-Type': 'text/plain'});
+  res.end('Hello World\n');
+}
+
+var http  = require('http'),
+    weak    = require('weak'),
+    done    = 0,
+    count   = 0,
+    countGC = 0,
+    todo    = 5,
+    common = require('../common.js'),
+    assert = require('assert'),
+    PORT = common.PORT;
+
+console.log('We should do '+ todo +' requests');
+
+var http = require('http');
+var server = http.createServer(serverHandler);
+server.listen(PORT, getall);
+
+
+function getall() {
+  for (var i = 0; i < todo; i++) {
+    (function(){
+      function cb(res) {
+        console.error('in cb')
+        done+=1;
+        res.on('end', statusLater);
+      }
+
+      var req = http.get({
+        hostname: 'localhost',
+        pathname: '/',
+        port: PORT
+      }, cb)
+
+      count++;
+      weak(req, afterGC);
+    })()
+  }
+}
+
+function afterGC(){
+  countGC ++;
+}
+
+function statusLater() {
+  setTimeout(status, 1);
+}
+
+function status() {
+  gc();
+  console.log('Done: %d/%d', done, todo);
+  console.log('Collected: %d/%d', countGC, count);
+  if (done === todo) {
+    console.log('All should be collected now.');
+    assert(count === countGC);
+    process.exit(0);
+  }
+}
diff --git a/test/gc/test-net-timeout.js b/test/gc/test-net-timeout.js
new file mode 100644 (file)
index 0000000..789193e
--- /dev/null
@@ -0,0 +1,61 @@
+// just like test/gc/http-client-timeout.js,
+// but using a net server/client instead
+
+function serverHandler(sock) {
+  sock.setTimeout(120000);
+  setTimeout(function () {
+    sock.end('hello\n');
+  }, 100);
+}
+
+var net  = require('net'),
+    weak    = require('weak'),
+    done    = 0,
+    count   = 0,
+    countGC = 0,
+    todo    = 18,
+    common = require('../common.js'),
+    assert = require('assert'),
+    PORT = common.PORT;
+
+console.log('We should do '+ todo +' requests');
+
+var server = net.createServer(serverHandler);
+server.listen(PORT, getall);
+
+function getall() {
+  for (var i = 0; i < todo; i++) {
+    (function(){
+      var req = net.connect(PORT, '127.0.0.1');
+      req.setTimeout(10, function() {
+        console.log('timeout (expected)')
+        req.destroy();
+        done++;
+        statusLater();
+      });
+
+      count++;
+      weak(req, afterGC);
+    })()
+  }
+}
+
+function afterGC(){
+  countGC ++;
+}
+
+function statusLater() {
+  setTimeout(status, 1);
+}
+
+function status() {
+  gc();
+  console.log('Done: %d/%d', done, todo);
+  console.log('Collected: %d/%d', countGC, count);
+  if (done === todo) {
+    console.log('All should be collected now.');
+    assert(count === countGC);
+    process.exit(0);
+  }
+}
+
diff --git a/test/gc/testcfg.py b/test/gc/testcfg.py
new file mode 100644 (file)
index 0000000..30f3bdb
--- /dev/null
@@ -0,0 +1,133 @@
+# Copyright 2008 the V8 project authors. All rights reserved.
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+#       notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+#       copyright notice, this list of conditions and the following
+#       disclaimer in the documentation and/or other materials provided
+#       with the distribution.
+#     * Neither the name of Google Inc. nor the names of its
+#       contributors may be used to endorse or promote products derived
+#       from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import test
+import os
+import shutil
+from shutil import rmtree
+from os import mkdir
+from glob import glob
+from os.path import join, dirname, exists
+import re
+
+
+FLAGS_PATTERN = re.compile(r"//\s+Flags:(.*)")
+FILES_PATTERN = re.compile(r"//\s+Files:(.*)")
+
+
+class GCTestCase(test.TestCase):
+
+  def __init__(self, path, file, mode, context, config):
+    super(GCTestCase, self).__init__(context, path, mode)
+    self.file = file
+    self.config = config
+    self.mode = mode
+    self.tmpdir = join(dirname(self.config.root), 'tmp')
+  
+  def AfterRun(self, result):
+    # delete the whole tmp dir
+    try:
+      rmtree(self.tmpdir)
+    except:
+      pass
+    # make it again.
+    try:
+      mkdir(self.tmpdir)
+    except:
+      pass
+
+  def BeforeRun(self):
+    # delete the whole tmp dir
+    try:
+      rmtree(self.tmpdir)
+    except:
+      pass
+    # make it again.
+    # intermittently fails on win32, so keep trying
+    while not os.path.exists(self.tmpdir):
+      try:
+        mkdir(self.tmpdir)
+      except:
+        pass
+  
+  def GetLabel(self):
+    return "%s %s" % (self.mode, self.GetName())
+
+  def GetName(self):
+    return self.path[-1]
+
+  def GetCommand(self):
+    result = [self.config.context.GetVm(self.mode)]
+    source = open(self.file).read()
+    flags_match = FLAGS_PATTERN.search(source)
+    if flags_match:
+      result += flags_match.group(1).strip().split()
+    files_match = FILES_PATTERN.search(source);
+    additional_files = []
+    if files_match:
+      additional_files += files_match.group(1).strip().split()
+    for a_file in additional_files:
+      result.append(join(dirname(self.config.root), '..', a_file))
+    result += ["--expose-gc"]
+    result += [self.file]
+    return result
+
+  def GetSource(self):
+    return open(self.file).read()
+
+
+class GCTestConfiguration(test.TestConfiguration):
+
+  def __init__(self, context, root):
+    super(GCTestConfiguration, self).__init__(context, root)
+
+  def Ls(self, path):
+    def SelectTest(name):
+      return name.startswith('test-') and name.endswith('.js')
+    return [f[:-3] for f in os.listdir(path) if SelectTest(f)]
+
+  def ListTests(self, current_path, path, mode):
+    all_tests = [current_path + [t] for t in self.Ls(join(self.root))]
+    result = []
+    for test in all_tests:
+      if self.Contains(path, test):
+        file_path = join(self.root, reduce(join, test[1:], "") + ".js")
+        result.append(GCTestCase(test, file_path, mode, self.context, self))
+    return result
+
+  def GetBuildRequirements(self):
+    return ['sample', 'sample=shell']
+
+  def GetTestStatus(self, sections, defs):
+    status_file = join(self.root, 'gc.status')
+    if exists(status_file):
+      test.ReadConfigurationInto(status_file, sections, defs)
+
+
+
+def GetConfiguration(context, root):
+  return GCTestConfiguration(context, root)
index b0cd9ab..d711f9c 100755 (executable)
@@ -1275,7 +1275,7 @@ def GetSpecialCommandProcessor(value):
     return ExpandCommand
 
 
-BUILT_IN_TESTS = ['simple', 'pummel', 'message', 'internet']
+BUILT_IN_TESTS = ['simple', 'pummel', 'message', 'internet', 'gc']
 
 
 def GetSuites(test_root):