From 91120e04297009ddabb6b7c68ec83a092041903c Mon Sep 17 00:00:00 2001 From: isaacs Date: Thu, 3 May 2012 10:16:25 -0700 Subject: [PATCH] Tests for memory leaks --- .gitignore | 1 + Makefile | 13 ++- test/gc/test-http-client-connaborted.js | 61 +++++++++++ test/gc/test-http-client-onerror.js | 66 ++++++++++++ test/gc/test-http-client-timeout.js | 69 ++++++++++++ test/gc/test-http-client.js | 63 +++++++++++ test/gc/test-net-timeout.js | 61 +++++++++++ test/gc/testcfg.py | 133 ++++++++++++++++++++++++ tools/test.py | 2 +- 9 files changed, 466 insertions(+), 3 deletions(-) create mode 100644 test/gc/test-http-client-connaborted.js create mode 100644 test/gc/test-http-client-onerror.js create mode 100644 test/gc/test-http-client-timeout.js create mode 100644 test/gc/test-http-client.js create mode 100644 test/gc/test-net-timeout.js create mode 100644 test/gc/testcfg.py diff --git a/.gitignore b/.gitignore index 3b2ad3f45a..937bfb37ad 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ ipch/ email.md blog.html deps/v8-* +node_modules diff --git a/Makefile b/Makefile index 0eb46052d2..5bd675c334 100644 --- 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 index 0000000000..84d7ca588e --- /dev/null +++ b/test/gc/test-http-client-connaborted.js @@ -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 index 0000000000..58724893a9 --- /dev/null +++ b/test/gc/test-http-client-onerror.js @@ -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 index 0000000000..32302a439d --- /dev/null +++ b/test/gc/test-http-client-timeout.js @@ -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 index 0000000000..913ca7b1af --- /dev/null +++ b/test/gc/test-http-client.js @@ -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 index 0000000000..789193ee37 --- /dev/null +++ b/test/gc/test-net-timeout.js @@ -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 index 0000000000..30f3bdbeb9 --- /dev/null +++ b/test/gc/testcfg.py @@ -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) diff --git a/tools/test.py b/tools/test.py index b0cd9abcd2..d711f9ca1d 100755 --- a/tools/test.py +++ b/tools/test.py @@ -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):