summaryrefslogtreecommitdiff
path: root/buildscripts
diff options
context:
space:
mode:
Diffstat (limited to 'buildscripts')
-rwxr-xr-xbuildscripts/packager.py16
-rwxr-xr-xbuildscripts/resmoke.py11
-rw-r--r--buildscripts/resmokeconfig/suites/aggregation.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/aggregation_ese.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/aggregation_facet_unwind_passthrough.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/aggregation_read_concern_majority_passthrough.yml5
-rw-r--r--buildscripts/resmokeconfig/suites/aggregation_sharded_collections_passthrough.yml4
-rw-r--r--buildscripts/resmokeconfig/suites/bulk_gle_passthrough.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/concurrency.yml4
-rw-r--r--buildscripts/resmokeconfig/suites/concurrency_replication.yml2
-rw-r--r--buildscripts/resmokeconfig/suites/concurrency_sharded.yml2
-rw-r--r--buildscripts/resmokeconfig/suites/core.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/core_auth.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/core_compression.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/core_ese.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/core_minimum_batch_size.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/core_op_query.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/decimal.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/disk.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/dur_jscore_passthrough.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/httpinterface.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/integration_tests_replset.yml5
-rw-r--r--buildscripts/resmokeconfig/suites/integration_tests_sharded.yml4
-rw-r--r--buildscripts/resmokeconfig/suites/integration_tests_standalone.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/jstestfuzz.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/jstestfuzz_replication.yml5
-rw-r--r--buildscripts/resmokeconfig/suites/jstestfuzz_replication_initsync.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/jstestfuzz_replication_resync.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/jstestfuzz_sharded.yml4
-rw-r--r--buildscripts/resmokeconfig/suites/master_slave_jscore_passthrough.yml4
-rw-r--r--buildscripts/resmokeconfig/suites/mmap.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/no_passthrough.yml4
-rw-r--r--buildscripts/resmokeconfig/suites/no_passthrough_with_mongod.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/parallel.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/read_concern_majority_passthrough.yml5
-rw-r--r--buildscripts/resmokeconfig/suites/replica_sets_initsync_jscore_passthrough.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/replica_sets_initsync_static_jscore_passthrough.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/replica_sets_jscore_passthrough.yml5
-rw-r--r--buildscripts/resmokeconfig/suites/replica_sets_kill_secondaries_jscore_passthrough.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/replica_sets_resync_jscore_passthrough.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/replica_sets_resync_static_jscore_passthrough.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/rlp.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/serial_run.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/sharded_collections_jscore_passthrough.yml4
-rw-r--r--buildscripts/resmokeconfig/suites/sharding_gle_auth_basics_passthrough.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/sharding_jscore_passthrough.yml4
-rw-r--r--buildscripts/resmokeconfig/suites/slow1.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/write_concern_majority_passthrough.yml5
-rw-r--r--buildscripts/resmokelib/config.py44
-rw-r--r--buildscripts/resmokelib/parser.py73
-rw-r--r--buildscripts/resmokelib/testing/executor.py12
-rw-r--r--buildscripts/resmokelib/testing/fixtures/interface.py14
-rw-r--r--buildscripts/resmokelib/testing/fixtures/masterslave.py9
-rw-r--r--buildscripts/resmokelib/testing/fixtures/replicaset.py9
-rw-r--r--buildscripts/resmokelib/testing/fixtures/shardedcluster.py33
-rw-r--r--buildscripts/resmokelib/testing/fixtures/standalone.py20
-rw-r--r--buildscripts/resmokelib/testing/hook_test_archival.py116
-rw-r--r--buildscripts/resmokelib/testing/job.py47
-rw-r--r--buildscripts/resmokelib/testing/report.py14
-rw-r--r--buildscripts/resmokelib/testing/testcases.py14
-rw-r--r--buildscripts/resmokelib/utils/__init__.py26
-rw-r--r--buildscripts/resmokelib/utils/archival.py331
-rw-r--r--buildscripts/tests/resmokelib/test_archival.py251
-rw-r--r--buildscripts/tests/resmokelib/utils/__init__.py1
-rw-r--r--buildscripts/tests/resmokelib/utils/test_rmtree.py126
65 files changed, 1246 insertions, 77 deletions
diff --git a/buildscripts/packager.py b/buildscripts/packager.py
index dcc95726398..d46d7e193c3 100755
--- a/buildscripts/packager.py
+++ b/buildscripts/packager.py
@@ -258,9 +258,7 @@ class Distro(object):
else:
raise Exception("unsupported build_os: %s" % build_os)
elif self.n == 'debian':
- if build_os == 'debian71':
- return 'wheezy'
- elif build_os == 'debian81':
+ if build_os == 'debian81':
return 'jessie'
else:
raise Exception("unsupported build_os: %s" % build_os)
@@ -277,7 +275,7 @@ class Distro(object):
def build_os(self, arch):
"""Return the build os label in the binary package to download (e.g. "rhel55" for redhat,
- "ubuntu1204" for ubuntu, "debian71" for debian, "suse11" for suse, etc.)"""
+ "ubuntu1204" for ubuntu, "debian81" for debian, "suse11" for suse, etc.)"""
# Community builds only support amd64
if arch not in ['x86_64', 'ppc64le', 's390x', 'arm64']:
raise Exception("BUG: unsupported architecture (%s)" % arch)
@@ -291,7 +289,7 @@ class Distro(object):
elif self.n == 'ubuntu':
return [ "ubuntu1204", "ubuntu1404", "ubuntu1604", ]
elif self.n == 'debian':
- return [ "debian71", "debian81" ]
+ return [ "debian81" ]
else:
raise Exception("BUG: unsupported platform?")
@@ -471,12 +469,8 @@ def make_deb(distro, build_os, arch, spec, srcdir):
sdir=setupdir(distro, build_os, arch, spec)
if re.search("debian", distro.name()):
os.unlink(sdir+"debian/mongod.upstart")
- if build_os == "debian71":
- os.link(sdir+"debian/init.d", sdir+"debian/%s%s-server.mongod.init" % (distro.pkgbase(), suffix))
- os.unlink(sdir+"debian/mongod.service")
- else:
- os.link(sdir+"debian/mongod.service", sdir+"debian/%s%s-server.mongod.service" % (distro.pkgbase(), suffix))
- os.unlink(sdir+"debian/init.d")
+ os.link(sdir+"debian/mongod.service", sdir+"debian/%s%s-server.mongod.service" % (distro.pkgbase(), suffix))
+ os.unlink(sdir+"debian/init.d")
elif re.search("ubuntu", distro.name()):
os.unlink(sdir+"debian/init.d")
if build_os in ("ubuntu1204", "ubuntu1404", "ubuntu1410"):
diff --git a/buildscripts/resmoke.py b/buildscripts/resmoke.py
index 7aa13277384..cee9ce4e391 100755
--- a/buildscripts/resmoke.py
+++ b/buildscripts/resmoke.py
@@ -54,10 +54,18 @@ def _execute_suite(suite, logging_config):
logger.info("Skipping %ss, no tests to run", group.test_kind)
continue
+ archive = None
+ if resmokelib.config.ARCHIVE_FILE:
+ archive = resmokelib.utils.archival.Archival(
+ archival_json_file=resmokelib.config.ARCHIVE_FILE,
+ limit_size_mb=resmokelib.config.ARCHIVE_LIMIT_MB,
+ limit_files=resmokelib.config.ARCHIVE_LIMIT_TESTS,
+ logger=logger)
group_config = suite.get_executor_config().get(group.test_kind, {})
executor = resmokelib.testing.executor.TestGroupExecutor(logger,
group,
logging_config,
+ archive_instance=archive,
**group_config)
try:
@@ -73,6 +81,9 @@ def _execute_suite(suite, logging_config):
group.test_kind, suite.get_name())
suite.return_code = 2
return False
+ finally:
+ if archive:
+ archive.exit()
def _log_summary(logger, suites, time_taken):
diff --git a/buildscripts/resmokeconfig/suites/aggregation.yml b/buildscripts/resmokeconfig/suites/aggregation.yml
index c7a8c6e64be..0a775a2d93c 100644
--- a/buildscripts/resmokeconfig/suites/aggregation.yml
+++ b/buildscripts/resmokeconfig/suites/aggregation.yml
@@ -8,6 +8,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - ValidateCollections
config:
shell_options:
readMode: commands
diff --git a/buildscripts/resmokeconfig/suites/aggregation_ese.yml b/buildscripts/resmokeconfig/suites/aggregation_ese.yml
index 395a8a37898..a2d260d91d1 100644
--- a/buildscripts/resmokeconfig/suites/aggregation_ese.yml
+++ b/buildscripts/resmokeconfig/suites/aggregation_ese.yml
@@ -16,6 +16,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - ValidateCollections
config:
shell_options:
readMode: commands
diff --git a/buildscripts/resmokeconfig/suites/aggregation_facet_unwind_passthrough.yml b/buildscripts/resmokeconfig/suites/aggregation_facet_unwind_passthrough.yml
index 40d248aa22d..476ba72b05d 100644
--- a/buildscripts/resmokeconfig/suites/aggregation_facet_unwind_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/aggregation_facet_unwind_passthrough.yml
@@ -8,6 +8,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - ValidateCollections
config:
shell_options:
readMode: commands
diff --git a/buildscripts/resmokeconfig/suites/aggregation_read_concern_majority_passthrough.yml b/buildscripts/resmokeconfig/suites/aggregation_read_concern_majority_passthrough.yml
index 72b838b3c07..5eea3a717bb 100644
--- a/buildscripts/resmokeconfig/suites/aggregation_read_concern_majority_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/aggregation_read_concern_majority_passthrough.yml
@@ -31,6 +31,11 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - CheckReplOplogs
+ - CheckReplDBHash
+ - ValidateCollections
config:
shell_options:
global_vars:
diff --git a/buildscripts/resmokeconfig/suites/aggregation_sharded_collections_passthrough.yml b/buildscripts/resmokeconfig/suites/aggregation_sharded_collections_passthrough.yml
index 1cb834a63eb..c5b76c31ef5 100644
--- a/buildscripts/resmokeconfig/suites/aggregation_sharded_collections_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/aggregation_sharded_collections_passthrough.yml
@@ -29,6 +29,10 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - CheckReplDBHash
+ - ValidateCollections
config:
shell_options:
readMode: commands
diff --git a/buildscripts/resmokeconfig/suites/bulk_gle_passthrough.yml b/buildscripts/resmokeconfig/suites/bulk_gle_passthrough.yml
index 8e88985f671..309dd0364ea 100644
--- a/buildscripts/resmokeconfig/suites/bulk_gle_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/bulk_gle_passthrough.yml
@@ -7,6 +7,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - ValidateCollections
config:
shell_options:
readMode: legacy
diff --git a/buildscripts/resmokeconfig/suites/concurrency.yml b/buildscripts/resmokeconfig/suites/concurrency.yml
index e585902274d..4af6a5e31ee 100644
--- a/buildscripts/resmokeconfig/suites/concurrency.yml
+++ b/buildscripts/resmokeconfig/suites/concurrency.yml
@@ -12,6 +12,10 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - ValidateCollections
+ tests: true
config:
shell_options:
readMode: commands
diff --git a/buildscripts/resmokeconfig/suites/concurrency_replication.yml b/buildscripts/resmokeconfig/suites/concurrency_replication.yml
index 8c9d272caea..9a575e63356 100644
--- a/buildscripts/resmokeconfig/suites/concurrency_replication.yml
+++ b/buildscripts/resmokeconfig/suites/concurrency_replication.yml
@@ -6,6 +6,8 @@ selector:
# Concurrency tests that run against a replica set start one themselves.
executor:
js_test:
+ archive:
+ tests: true
config:
shell_options:
nodb: ''
diff --git a/buildscripts/resmokeconfig/suites/concurrency_sharded.yml b/buildscripts/resmokeconfig/suites/concurrency_sharded.yml
index b5bd8b7169e..0e219f2f26c 100644
--- a/buildscripts/resmokeconfig/suites/concurrency_sharded.yml
+++ b/buildscripts/resmokeconfig/suites/concurrency_sharded.yml
@@ -6,6 +6,8 @@ selector:
# Concurrency tests that run against a sharded cluster start one themselves.
executor:
js_test:
+ archive:
+ tests: true
config:
shell_options:
nodb: ''
diff --git a/buildscripts/resmokeconfig/suites/core.yml b/buildscripts/resmokeconfig/suites/core.yml
index 04a6f97cb7d..9ba9ee4b742 100644
--- a/buildscripts/resmokeconfig/suites/core.yml
+++ b/buildscripts/resmokeconfig/suites/core.yml
@@ -5,6 +5,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - ValidateCollections
config:
shell_options:
readMode: commands
diff --git a/buildscripts/resmokeconfig/suites/core_auth.yml b/buildscripts/resmokeconfig/suites/core_auth.yml
index 72c6c716927..cca107d5afa 100644
--- a/buildscripts/resmokeconfig/suites/core_auth.yml
+++ b/buildscripts/resmokeconfig/suites/core_auth.yml
@@ -18,6 +18,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - ValidateCollections
config:
shell_options:
global_vars:
diff --git a/buildscripts/resmokeconfig/suites/core_compression.yml b/buildscripts/resmokeconfig/suites/core_compression.yml
index d09716735da..32e3d381bbc 100644
--- a/buildscripts/resmokeconfig/suites/core_compression.yml
+++ b/buildscripts/resmokeconfig/suites/core_compression.yml
@@ -5,6 +5,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - ValidateCollections
config:
shell_options:
global_vars:
diff --git a/buildscripts/resmokeconfig/suites/core_ese.yml b/buildscripts/resmokeconfig/suites/core_ese.yml
index 74ab7c9b195..86aa646bdb7 100644
--- a/buildscripts/resmokeconfig/suites/core_ese.yml
+++ b/buildscripts/resmokeconfig/suites/core_ese.yml
@@ -9,6 +9,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - ValidateCollections
config:
shell_options:
readMode: commands
diff --git a/buildscripts/resmokeconfig/suites/core_minimum_batch_size.yml b/buildscripts/resmokeconfig/suites/core_minimum_batch_size.yml
index 085c32c8c29..3b5a179914b 100644
--- a/buildscripts/resmokeconfig/suites/core_minimum_batch_size.yml
+++ b/buildscripts/resmokeconfig/suites/core_minimum_batch_size.yml
@@ -13,6 +13,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - ValidateCollections
config:
shell_options:
global_vars:
diff --git a/buildscripts/resmokeconfig/suites/core_op_query.yml b/buildscripts/resmokeconfig/suites/core_op_query.yml
index 5ec056a57ff..45e57b65376 100644
--- a/buildscripts/resmokeconfig/suites/core_op_query.yml
+++ b/buildscripts/resmokeconfig/suites/core_op_query.yml
@@ -5,6 +5,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - ValidateCollections
config:
shell_options:
rpcProtocols: opQueryOnly
diff --git a/buildscripts/resmokeconfig/suites/decimal.yml b/buildscripts/resmokeconfig/suites/decimal.yml
index 62cb11ecab2..6c934554e42 100644
--- a/buildscripts/resmokeconfig/suites/decimal.yml
+++ b/buildscripts/resmokeconfig/suites/decimal.yml
@@ -5,6 +5,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - ValidateCollections
config:
shell_options:
readMode: commands
diff --git a/buildscripts/resmokeconfig/suites/disk.yml b/buildscripts/resmokeconfig/suites/disk.yml
index fd8a5fd54b1..845ce95ea8f 100644
--- a/buildscripts/resmokeconfig/suites/disk.yml
+++ b/buildscripts/resmokeconfig/suites/disk.yml
@@ -7,6 +7,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - ValidateCollections
config:
shell_options:
global_vars:
diff --git a/buildscripts/resmokeconfig/suites/dur_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/dur_jscore_passthrough.yml
index 347d23de967..f973f95586c 100644
--- a/buildscripts/resmokeconfig/suites/dur_jscore_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/dur_jscore_passthrough.yml
@@ -8,6 +8,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - ValidateCollections
config:
shell_options:
readMode: commands
diff --git a/buildscripts/resmokeconfig/suites/httpinterface.yml b/buildscripts/resmokeconfig/suites/httpinterface.yml
index 1f15a873236..c74801ccdf6 100644
--- a/buildscripts/resmokeconfig/suites/httpinterface.yml
+++ b/buildscripts/resmokeconfig/suites/httpinterface.yml
@@ -7,6 +7,9 @@ selector:
- jstests/httpinterface/httpinterface.js
executor:
js_test:
+ archive:
+ hooks:
+ - ValidateCollections
hooks:
- class: ValidateCollections
fixture:
diff --git a/buildscripts/resmokeconfig/suites/integration_tests_replset.yml b/buildscripts/resmokeconfig/suites/integration_tests_replset.yml
index 1345b567eeb..f4d3e6e312e 100644
--- a/buildscripts/resmokeconfig/suites/integration_tests_replset.yml
+++ b/buildscripts/resmokeconfig/suites/integration_tests_replset.yml
@@ -4,6 +4,11 @@ selector:
executor:
cpp_integration_test:
+ archive:
+ hooks:
+ - CheckReplOplogs
+ - CheckReplDBHash
+ - ValidateCollections
config: {}
hooks:
# The CheckReplDBHash hook waits until all operations have replicated to and have been applied
diff --git a/buildscripts/resmokeconfig/suites/integration_tests_sharded.yml b/buildscripts/resmokeconfig/suites/integration_tests_sharded.yml
index a0e8c16be9b..444c66aa30d 100644
--- a/buildscripts/resmokeconfig/suites/integration_tests_sharded.yml
+++ b/buildscripts/resmokeconfig/suites/integration_tests_sharded.yml
@@ -6,6 +6,10 @@ selector:
executor:
cpp_integration_test:
+ archive:
+ hooks:
+ - CheckReplDBHash
+ - ValidateCollections
config: {}
hooks:
- class: CheckReplDBHash
diff --git a/buildscripts/resmokeconfig/suites/integration_tests_standalone.yml b/buildscripts/resmokeconfig/suites/integration_tests_standalone.yml
index c594fc6fae1..127a7535568 100644
--- a/buildscripts/resmokeconfig/suites/integration_tests_standalone.yml
+++ b/buildscripts/resmokeconfig/suites/integration_tests_standalone.yml
@@ -4,6 +4,9 @@ selector:
executor:
cpp_integration_test:
+ archive:
+ hooks:
+ - ValidateCollections
config: {}
hooks:
- class: ValidateCollections
diff --git a/buildscripts/resmokeconfig/suites/jstestfuzz.yml b/buildscripts/resmokeconfig/suites/jstestfuzz.yml
index 872c2a1f229..591255038f3 100644
--- a/buildscripts/resmokeconfig/suites/jstestfuzz.yml
+++ b/buildscripts/resmokeconfig/suites/jstestfuzz.yml
@@ -5,6 +5,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - ValidateCollections
config:
shell_options:
readMode: commands
diff --git a/buildscripts/resmokeconfig/suites/jstestfuzz_replication.yml b/buildscripts/resmokeconfig/suites/jstestfuzz_replication.yml
index a482038899f..f32a24e4ee8 100644
--- a/buildscripts/resmokeconfig/suites/jstestfuzz_replication.yml
+++ b/buildscripts/resmokeconfig/suites/jstestfuzz_replication.yml
@@ -5,6 +5,11 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - CheckReplOplogs
+ - CheckReplDBHash
+ - ValidateCollections
config:
shell_options:
readMode: commands
diff --git a/buildscripts/resmokeconfig/suites/jstestfuzz_replication_initsync.yml b/buildscripts/resmokeconfig/suites/jstestfuzz_replication_initsync.yml
index 034f254494f..d38814a0008 100644
--- a/buildscripts/resmokeconfig/suites/jstestfuzz_replication_initsync.yml
+++ b/buildscripts/resmokeconfig/suites/jstestfuzz_replication_initsync.yml
@@ -5,6 +5,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - BackgroundInitialSync
config:
shell_options:
readMode: commands
diff --git a/buildscripts/resmokeconfig/suites/jstestfuzz_replication_resync.yml b/buildscripts/resmokeconfig/suites/jstestfuzz_replication_resync.yml
index 1038b99a10c..973914a3c01 100644
--- a/buildscripts/resmokeconfig/suites/jstestfuzz_replication_resync.yml
+++ b/buildscripts/resmokeconfig/suites/jstestfuzz_replication_resync.yml
@@ -5,6 +5,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - BackgroundInitialSync
config:
shell_options:
readMode: commands
diff --git a/buildscripts/resmokeconfig/suites/jstestfuzz_sharded.yml b/buildscripts/resmokeconfig/suites/jstestfuzz_sharded.yml
index 9a8a02db6b9..bf2f84fe373 100644
--- a/buildscripts/resmokeconfig/suites/jstestfuzz_sharded.yml
+++ b/buildscripts/resmokeconfig/suites/jstestfuzz_sharded.yml
@@ -5,6 +5,10 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - CheckReplDBHash
+ - ValidateCollections
config:
shell_options:
readMode: commands
diff --git a/buildscripts/resmokeconfig/suites/master_slave_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/master_slave_jscore_passthrough.yml
index a46c0cf29ed..1de4c693009 100644
--- a/buildscripts/resmokeconfig/suites/master_slave_jscore_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/master_slave_jscore_passthrough.yml
@@ -10,6 +10,10 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - CheckReplDBHash
+ - ValidateCollections
config:
shell_options:
eval: "testingReplication = true;"
diff --git a/buildscripts/resmokeconfig/suites/mmap.yml b/buildscripts/resmokeconfig/suites/mmap.yml
index 6fc951c25cb..0aefad2ee53 100644
--- a/buildscripts/resmokeconfig/suites/mmap.yml
+++ b/buildscripts/resmokeconfig/suites/mmap.yml
@@ -5,6 +5,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - ValidateCollections
config:
shell_options:
global_vars:
diff --git a/buildscripts/resmokeconfig/suites/no_passthrough.yml b/buildscripts/resmokeconfig/suites/no_passthrough.yml
index 2afbe6ae044..8012bfaf155 100644
--- a/buildscripts/resmokeconfig/suites/no_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/no_passthrough.yml
@@ -9,6 +9,10 @@ selector:
# noPassthrough tests start their own mongod's.
executor:
js_test:
+ archive:
+ tests:
+ - jstests/noPassthrough/backup*.js
+ - jstests/noPassthrough/wt_unclean_shutdown.js
config:
shell_options:
nodb: ''
diff --git a/buildscripts/resmokeconfig/suites/no_passthrough_with_mongod.yml b/buildscripts/resmokeconfig/suites/no_passthrough_with_mongod.yml
index fa8e62813d1..4e8bfc22bab 100644
--- a/buildscripts/resmokeconfig/suites/no_passthrough_with_mongod.yml
+++ b/buildscripts/resmokeconfig/suites/no_passthrough_with_mongod.yml
@@ -5,6 +5,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - ValidateCollections
config:
shell_options:
readMode: commands
diff --git a/buildscripts/resmokeconfig/suites/parallel.yml b/buildscripts/resmokeconfig/suites/parallel.yml
index 7f42d19c18c..9611ec55c48 100644
--- a/buildscripts/resmokeconfig/suites/parallel.yml
+++ b/buildscripts/resmokeconfig/suites/parallel.yml
@@ -5,6 +5,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - ValidateCollections
config:
shell_options:
readMode: commands
diff --git a/buildscripts/resmokeconfig/suites/read_concern_majority_passthrough.yml b/buildscripts/resmokeconfig/suites/read_concern_majority_passthrough.yml
index 22e84bc9c63..d469ff85f75 100644
--- a/buildscripts/resmokeconfig/suites/read_concern_majority_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/read_concern_majority_passthrough.yml
@@ -28,6 +28,11 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - CheckReplOplogs
+ - CheckReplDBHash
+ - ValidateCollections
config:
shell_options:
global_vars:
diff --git a/buildscripts/resmokeconfig/suites/replica_sets_initsync_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_initsync_jscore_passthrough.yml
index 114ebcab372..034c0812e90 100644
--- a/buildscripts/resmokeconfig/suites/replica_sets_initsync_jscore_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/replica_sets_initsync_jscore_passthrough.yml
@@ -73,6 +73,9 @@ selector:
run_hook_interval: &run_hook_interval 20
executor:
js_test:
+ archive:
+ hooks:
+ - BackgroundInitialSync
config:
shell_options:
eval: "testingReplication = true;"
diff --git a/buildscripts/resmokeconfig/suites/replica_sets_initsync_static_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_initsync_static_jscore_passthrough.yml
index c15b23046a9..8fce4506393 100644
--- a/buildscripts/resmokeconfig/suites/replica_sets_initsync_static_jscore_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/replica_sets_initsync_static_jscore_passthrough.yml
@@ -12,6 +12,9 @@ selector:
run_hook_interval: &run_hook_interval 20
executor:
js_test:
+ archive:
+ hooks:
+ - IntermediateInitialSync
config:
shell_options:
eval: "testingReplication = true;"
diff --git a/buildscripts/resmokeconfig/suites/replica_sets_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_jscore_passthrough.yml
index f487c1aa113..60da231d3bc 100644
--- a/buildscripts/resmokeconfig/suites/replica_sets_jscore_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/replica_sets_jscore_passthrough.yml
@@ -11,6 +11,11 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - CheckReplOplogs
+ - CheckReplDBHash
+ - ValidateCollections
config:
shell_options:
eval: "testingReplication = true;"
diff --git a/buildscripts/resmokeconfig/suites/replica_sets_kill_secondaries_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_kill_secondaries_jscore_passthrough.yml
index a4b6c5a5feb..e62727bb606 100644
--- a/buildscripts/resmokeconfig/suites/replica_sets_kill_secondaries_jscore_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/replica_sets_kill_secondaries_jscore_passthrough.yml
@@ -15,6 +15,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - PeriodicKillSecondaries
config:
shell_options:
eval: "testingReplication = true;"
diff --git a/buildscripts/resmokeconfig/suites/replica_sets_resync_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_resync_jscore_passthrough.yml
index 1e51713a475..c7d605d1184 100644
--- a/buildscripts/resmokeconfig/suites/replica_sets_resync_jscore_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/replica_sets_resync_jscore_passthrough.yml
@@ -73,6 +73,9 @@ selector:
run_hook_interval: &run_hook_interval 20
executor:
js_test:
+ archive:
+ hooks:
+ - BackgroundInitialSync
config:
shell_options:
eval: "testingReplication = true;"
diff --git a/buildscripts/resmokeconfig/suites/replica_sets_resync_static_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_resync_static_jscore_passthrough.yml
index dc2059879d3..bd26f1c23f8 100644
--- a/buildscripts/resmokeconfig/suites/replica_sets_resync_static_jscore_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/replica_sets_resync_static_jscore_passthrough.yml
@@ -12,6 +12,9 @@ selector:
run_hook_interval: &run_hook_interval 20
executor:
js_test:
+ archive:
+ hooks:
+ - IntermediateInitialSync
config:
shell_options:
eval: "testingReplication = true;"
diff --git a/buildscripts/resmokeconfig/suites/rlp.yml b/buildscripts/resmokeconfig/suites/rlp.yml
index ef0dfeda45a..16413f1d4a8 100644
--- a/buildscripts/resmokeconfig/suites/rlp.yml
+++ b/buildscripts/resmokeconfig/suites/rlp.yml
@@ -5,6 +5,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - ValidateCollections
config:
shell_options:
readMode: commands
diff --git a/buildscripts/resmokeconfig/suites/serial_run.yml b/buildscripts/resmokeconfig/suites/serial_run.yml
index 4ae010c7ec8..c25e61a980a 100644
--- a/buildscripts/resmokeconfig/suites/serial_run.yml
+++ b/buildscripts/resmokeconfig/suites/serial_run.yml
@@ -5,6 +5,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - ValidateCollections
config:
shell_options:
readMode: commands
diff --git a/buildscripts/resmokeconfig/suites/sharded_collections_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/sharded_collections_jscore_passthrough.yml
index 96674cfc20c..104707d1ac8 100644
--- a/buildscripts/resmokeconfig/suites/sharded_collections_jscore_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/sharded_collections_jscore_passthrough.yml
@@ -69,6 +69,10 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - CheckReplDBHash
+ - ValidateCollections
config:
shell_options:
readMode: commands
diff --git a/buildscripts/resmokeconfig/suites/sharding_gle_auth_basics_passthrough.yml b/buildscripts/resmokeconfig/suites/sharding_gle_auth_basics_passthrough.yml
index 8879e39f143..5f646fd7653 100644
--- a/buildscripts/resmokeconfig/suites/sharding_gle_auth_basics_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/sharding_gle_auth_basics_passthrough.yml
@@ -19,6 +19,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - CheckReplDBHash
config:
shell_options:
global_vars:
diff --git a/buildscripts/resmokeconfig/suites/sharding_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/sharding_jscore_passthrough.yml
index 496bd265a58..d2cb6ed55f1 100644
--- a/buildscripts/resmokeconfig/suites/sharding_jscore_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/sharding_jscore_passthrough.yml
@@ -51,6 +51,10 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - CheckReplDBHash
+ - ValidateCollections
config:
shell_options:
readMode: commands
diff --git a/buildscripts/resmokeconfig/suites/slow1.yml b/buildscripts/resmokeconfig/suites/slow1.yml
index 10c399ec9e5..8a764c39d94 100644
--- a/buildscripts/resmokeconfig/suites/slow1.yml
+++ b/buildscripts/resmokeconfig/suites/slow1.yml
@@ -5,6 +5,9 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - ValidateCollections
config:
shell_options:
readMode: commands
diff --git a/buildscripts/resmokeconfig/suites/write_concern_majority_passthrough.yml b/buildscripts/resmokeconfig/suites/write_concern_majority_passthrough.yml
index 388046d2782..b19989e3656 100644
--- a/buildscripts/resmokeconfig/suites/write_concern_majority_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/write_concern_majority_passthrough.yml
@@ -46,6 +46,11 @@ selector:
executor:
js_test:
+ archive:
+ hooks:
+ - CheckReplOplogs
+ - CheckReplDBHash
+ - ValidateCollections
config:
shell_options:
global_vars:
diff --git a/buildscripts/resmokelib/config.py b/buildscripts/resmokelib/config.py
index b75753398a5..8f636ddbadd 100644
--- a/buildscripts/resmokelib/config.py
+++ b/buildscripts/resmokelib/config.py
@@ -33,14 +33,20 @@ MONGO_RUNNER_SUBDIR = "mongorunner"
# Names below correspond to how they are specified via the command line or in the options YAML file.
DEFAULTS = {
+ "archiveFile": None,
+ "archiveLimitMb": 5000,
+ "archiveLimitTests": 10,
"basePort": 20000,
"buildloggerUrl": "https://logkeeper.mongodb.org",
"continueOnFailure": False,
"dbpathPrefix": None,
"dbtest": None,
+ "distroId": None,
"dryRun": None,
"excludeWithAllTags": None,
"excludeWithAnyTags": None,
+ "executionNumber": 0,
+ "gitRevision": None,
"includeWithAllTags": None,
"includeWithAnyTags": None,
"jobs": 1,
@@ -51,6 +57,7 @@ DEFAULTS = {
"mongosSetParameters": None,
"nojournal": False,
"numClientsPerFixture": 1,
+ "projectName": "mongodb-mongo-master",
"repeat": 1,
"reportFile": None,
"seed": long(time.time() * 256), # Taken from random.py code in Python 2.7.
@@ -59,6 +66,9 @@ DEFAULTS = {
"shuffle": False,
"storageEngine": None,
"storageEngineCacheSizeGB": None,
+ "taskId": None,
+ "taskName": None,
+ "variantName": None,
"wiredTigerCollectionConfigString": None,
"wiredTigerEngineConfigString": None,
"wiredTigerIndexConfigString": None
@@ -69,6 +79,15 @@ DEFAULTS = {
# Variables that are set by the user at the command line or with --options.
##
+# The name of the archive JSON file used to associate S3 archives to an Evergreen task.
+ARCHIVE_FILE = None
+
+# The limit size of all archive files for an Evergreen task.
+ARCHIVE_LIMIT_MB = None
+
+# The limit number of tests to archive for an Evergreen task.
+ARCHIVE_LIMIT_TESTS = None
+
# The starting port number to use for mongod and mongos processes spawned by resmoke.py and the
# mongo shell.
BASE_PORT = None
@@ -87,6 +106,28 @@ DBTEST_EXECUTABLE = None
# actually running them).
DRY_RUN = None
+# The identifier for the Evergreen distro that resmoke.py is being run on.
+EVERGREEN_DISTRO_ID = None
+
+# The number of the Evergreen execution that resmoke.py is being run on.
+EVERGREEN_EXECUTION = None
+
+# The name of the Evergreen project that resmoke.py is being run on.
+EVERGREEN_PROJECT_NAME = None
+
+# The git revision of the Evergreen task that resmoke.py is being run on.
+EVERGREEN_REVISION = None
+
+# The identifier for the Evergreen task that resmoke.py is being run under. If set, then the
+# Evergreen task id value will be transmitted to logkeeper when creating builds and tests.
+EVERGREEN_TASK_ID = None
+
+# The name of the Evergreen task that resmoke.py is being run for.
+EVERGREEN_TASK_NAME = None
+
+# The name of the Evergreen build variant that resmoke.py is being run on.
+EVERGREEN_VARIANT_NAME = None
+
# If set, then any jstests that have all of the specified tags will be excluded from the suite(s).
EXCLUDE_WITH_ALL_TAGS = None
@@ -177,6 +218,9 @@ WT_INDEX_CONFIG = None
# Internally used configuration options that aren't exposed to the user
##
+# S3 Bucket to upload archive files.
+ARCHIVE_BUCKET = "mongodatafiles"
+
# Default sort order for test execution. Will only be changed if --suites wasn't specified.
ORDER_TESTS_BY_NAME = True
diff --git a/buildscripts/resmokelib/parser.py b/buildscripts/resmokelib/parser.py
index 870e0e9145c..59b4585a273 100644
--- a/buildscripts/resmokelib/parser.py
+++ b/buildscripts/resmokelib/parser.py
@@ -18,14 +18,20 @@ from .. import resmokeconfig
# Mapping of the attribute of the parsed arguments (dest) to its key as it appears in the options
# YAML configuration file. Most should only be converting from snake_case to camelCase.
DEST_TO_CONFIG = {
+ "archive_file": "archiveFile",
+ "archive_limit_mb": "archiveLimitMb",
+ "archive_limit_tests": "archiveLimitTests",
"base_port": "basePort",
"buildlogger_url": "buildloggerUrl",
"continue_on_failure": "continueOnFailure",
"dbpath_prefix": "dbpathPrefix",
"dbtest_executable": "dbtest",
+ "distro_id": "distroId",
"dry_run": "dryRun",
"exclude_with_all_tags": "excludeWithAllTags",
"exclude_with_any_tags": "excludeWithAnyTags",
+ "execution_number": "executionNumber",
+ "git_revision": "gitRevision",
"include_with_all_tags": "includeWithAllTags",
"include_with_any_tags": "includeWithAnyTags",
"jobs": "jobs",
@@ -37,6 +43,7 @@ DEST_TO_CONFIG = {
"no_journal": "nojournal",
"num_clients_per_fixture": "numClientsPerFixture",
"prealloc_journal": "preallocJournal",
+ "project_name": "projectName",
"repeat": "repeat",
"report_file": "reportFile",
"seed": "seed",
@@ -45,6 +52,9 @@ DEST_TO_CONFIG = {
"shuffle": "shuffle",
"storage_engine": "storageEngine",
"storage_engine_cache_size": "storageEngineCacheSizeGB",
+ "task_id": "taskId",
+ "task_name": "taskName",
+ "variant_name": "variantName",
"wt_coll_config": "wiredTigerCollectionConfigString",
"wt_engine_config": "wiredTigerEngineConfigString",
"wt_index_config": "wiredTigerIndexConfigString"
@@ -79,6 +89,23 @@ def parse_command_line():
parser.add_option("--options", dest="options_file", metavar="OPTIONS",
help="A YAML file that specifies global options to resmoke.py.")
+ parser.add_option("--archiveFile", dest="archive_file", metavar="ARCHIVE_FILE",
+ help=("Sets the archive file name for the Evergreen task running the tests."
+ " The archive file is JSON format containing a list of tests that were"
+ " successfully archived to S3. If unspecified, no data files from tests"
+ " will be archived in S3. Tests can be designated for archival in the"
+ " task suite configuration file."))
+
+ parser.add_option("--archiveLimitMb", type="int", dest="archive_limit_mb",
+ metavar="ARCHIVE_LIMIT_MB",
+ help=("Sets the limit (in MB) for archived files to S3. A value of 0"
+ " indicates there is no limit."))
+
+ parser.add_option("--archiveLimitTests", type="int", dest="archive_limit_tests",
+ metavar="ARCHIVE_LIMIT_TESTS",
+ help=("Sets the maximum number of tests to archive to S3. A value"
+ " of 0 indicates there is no limit."))
+
parser.add_option("--basePort", dest="base_port", metavar="PORT",
help=("The starting port number to use for mongod and mongos processes"
" spawned by resmoke.py or the tests themselves. Each fixture and Job"
@@ -209,6 +236,42 @@ def parse_command_line():
parser.add_option("--wiredTigerIndexConfigString", dest="wt_index_config", metavar="CONFIG",
help="Set the WiredTiger index configuration setting for all mongod's.")
+
+ evergreen_options = optparse.OptionGroup(
+ parser, title="Evergreen options",
+ description=("Options used to propagate information about the Evergreen task running this"
+ " script."))
+ parser.add_option_group(evergreen_options)
+
+ evergreen_options.add_option("--buildId", dest="build_id", metavar="BUILD_ID",
+ help="Sets the build ID of the task.")
+
+ evergreen_options.add_option("--distroId", dest="distro_id", metavar="DISTRO_ID",
+ help=("Sets the identifier for the Evergreen distro running the"
+ " tests."))
+
+ evergreen_options.add_option("--executionNumber", type="int", dest="execution_number",
+ metavar="EXECUTION_NUMBER",
+ help=("Sets the number for the Evergreen execution running the"
+ " tests."))
+
+ evergreen_options.add_option("--gitRevision", dest="git_revision", metavar="GIT_REVISION",
+ help=("Sets the git revision for the Evergreen task running the"
+ " tests."))
+
+ evergreen_options.add_option("--projectName", dest="project_name", metavar="PROJECT_NAME",
+ help=("Sets the name of the Evergreen project running the tests."))
+
+ evergreen_options.add_option("--taskName", dest="task_name", metavar="TASK_NAME",
+ help="Sets the name of the Evergreen task running the tests.")
+
+ evergreen_options.add_option("--taskId", dest="task_id", metavar="TASK_ID",
+ help="Sets the Id of the Evergreen task running the tests.")
+
+ evergreen_options.add_option("--variantName", dest="variant_name", metavar="VARIANT_NAME",
+ help=("Sets the name of the Evergreen build variant running the"
+ " tests."))
+
parser.set_defaults(executor_file="with_server",
logger_file="console",
dry_run="off",
@@ -237,11 +300,21 @@ def update_config_vars(values):
if values[dest] is not None:
config[config_var] = values[dest]
+ _config.ARCHIVE_FILE = config.pop("archiveFile")
+ _config.ARCHIVE_LIMIT_MB = config.pop("archiveLimitMb")
+ _config.ARCHIVE_LIMIT_TESTS = config.pop("archiveLimitTests")
_config.BASE_PORT = int(config.pop("basePort"))
_config.BUILDLOGGER_URL = config.pop("buildloggerUrl")
_config.DBPATH_PREFIX = _expand_user(config.pop("dbpathPrefix"))
_config.DBTEST_EXECUTABLE = _expand_user(config.pop("dbtest"))
_config.DRY_RUN = config.pop("dryRun")
+ _config.EVERGREEN_DISTRO_ID = config.pop("distroId")
+ _config.EVERGREEN_EXECUTION = config.pop("executionNumber")
+ _config.EVERGREEN_PROJECT_NAME = config.pop("projectName")
+ _config.EVERGREEN_REVISION = config.pop("gitRevision")
+ _config.EVERGREEN_TASK_ID = config.pop("taskId")
+ _config.EVERGREEN_TASK_NAME = config.pop("taskName")
+ _config.EVERGREEN_VARIANT_NAME = config.pop("variantName")
_config.EXCLUDE_WITH_ALL_TAGS = config.pop("excludeWithAllTags")
_config.EXCLUDE_WITH_ANY_TAGS = config.pop("excludeWithAnyTags")
_config.FAIL_FAST = not config.pop("continueOnFailure")
diff --git a/buildscripts/resmokelib/testing/executor.py b/buildscripts/resmokelib/testing/executor.py
index 438a06b9c8e..e88eed6bc02 100644
--- a/buildscripts/resmokelib/testing/executor.py
+++ b/buildscripts/resmokelib/testing/executor.py
@@ -8,6 +8,7 @@ import threading
import time
from . import fixtures
+from . import hook_test_archival as archival
from . import hooks as _hooks
from . import job as _job
from . import report as _report
@@ -35,7 +36,9 @@ class TestGroupExecutor(object):
logging_config,
config=None,
fixture=None,
- hooks=None):
+ hooks=None,
+ archive_instance=None,
+ archive=None):
"""
Initializes the TestGroupExecutor with the test group to run.
"""
@@ -49,6 +52,11 @@ class TestGroupExecutor(object):
self.hooks_config = utils.default_if_none(hooks, [])
self.test_config = utils.default_if_none(config, {})
+ self.archival = None
+ if archive_instance:
+ self.archival = archival.HookTestArchival(test_group, self.hooks_config,
+ archive_instance, archive)
+
self._test_group = test_group
self._using_buildlogger = logging.config.using_buildlogger(logging_config)
@@ -299,7 +307,7 @@ class TestGroupExecutor(object):
build_id=build_id,
build_config=build_config)
- return _job.Job(logger, fixture, hooks, report)
+ return _job.Job(logger, fixture, hooks, report, self.archival)
def _make_test_queue(self):
"""
diff --git a/buildscripts/resmokelib/testing/fixtures/interface.py b/buildscripts/resmokelib/testing/fixtures/interface.py
index f2cb1b9c8b8..5da95175008 100644
--- a/buildscripts/resmokelib/testing/fixtures/interface.py
+++ b/buildscripts/resmokelib/testing/fixtures/interface.py
@@ -4,12 +4,16 @@ Interface of the different fixtures for executing JSTests against.
from __future__ import absolute_import
+import os.path
import time
import pymongo
+import pymongo.errors
+from ... import config
from ... import errors
from ... import logging
+from ... import utils
class Fixture(object):
@@ -17,7 +21,7 @@ class Fixture(object):
Base class for all fixtures.
"""
- def __init__(self, logger, job_num):
+ def __init__(self, logger, job_num, dbpath_prefix=None):
"""
Initializes the fixture with a logger instance.
"""
@@ -32,9 +36,12 @@ class Fixture(object):
self.logger = logger
self.job_num = job_num
-
self.port = None # Port that the mongo shell should connect to.
+ dbpath_prefix = utils.default_if_none(config.DBPATH_PREFIX, dbpath_prefix)
+ dbpath_prefix = utils.default_if_none(dbpath_prefix, config.DEFAULT_DBPATH_PREFIX)
+ self._dbpath_prefix = os.path.join(dbpath_prefix, "job{}".format(self.job_num))
+
def setup(self):
"""
Creates the fixture.
@@ -79,6 +86,9 @@ class Fixture(object):
"""
return True
+ def get_dbpath_prefix(self):
+ return self._dbpath_prefix
+
def get_internal_connection_string(self):
"""
Returns the connection string for this fixture. This is NOT a
diff --git a/buildscripts/resmokelib/testing/fixtures/masterslave.py b/buildscripts/resmokelib/testing/fixtures/masterslave.py
index fb444cfe097..f8bf4417451 100644
--- a/buildscripts/resmokelib/testing/fixtures/masterslave.py
+++ b/buildscripts/resmokelib/testing/fixtures/masterslave.py
@@ -31,7 +31,7 @@ class MasterSlaveFixture(interface.ReplFixture):
dbpath_prefix=None,
preserve_dbpath=False):
- interface.ReplFixture.__init__(self, logger, job_num)
+ interface.ReplFixture.__init__(self, logger, job_num, dbpath_prefix=dbpath_prefix)
if "dbpath" in mongod_options:
raise ValueError("Cannot specify mongod_options.dbpath")
@@ -42,12 +42,7 @@ class MasterSlaveFixture(interface.ReplFixture):
self.slave_options = utils.default_if_none(slave_options, {})
self.preserve_dbpath = preserve_dbpath
- # Command line options override the YAML configuration.
- dbpath_prefix = utils.default_if_none(config.DBPATH_PREFIX, dbpath_prefix)
- dbpath_prefix = utils.default_if_none(dbpath_prefix, config.DEFAULT_DBPATH_PREFIX)
- self._dbpath_prefix = os.path.join(dbpath_prefix,
- "job%d" % (self.job_num),
- config.FIXTURE_SUBDIR)
+ self._dbpath_prefix = os.path.join(self._dbpath_prefix, config.FIXTURE_SUBDIR)
self.master = None
self.slave = None
diff --git a/buildscripts/resmokelib/testing/fixtures/replicaset.py b/buildscripts/resmokelib/testing/fixtures/replicaset.py
index 0ec3de30280..b185ee7f8f2 100644
--- a/buildscripts/resmokelib/testing/fixtures/replicaset.py
+++ b/buildscripts/resmokelib/testing/fixtures/replicaset.py
@@ -40,7 +40,7 @@ class ReplicaSetFixture(interface.ReplFixture):
voting_secondaries=False,
use_replica_set_connection_string=False):
- interface.ReplFixture.__init__(self, logger, job_num)
+ interface.ReplFixture.__init__(self, logger, job_num, dbpath_prefix=dbpath_prefix)
self.mongod_executable = mongod_executable
self.mongod_options = utils.default_if_none(mongod_options, {})
@@ -59,12 +59,7 @@ class ReplicaSetFixture(interface.ReplFixture):
if "dbpath" in self.mongod_options:
self._dbpath_prefix = self.mongod_options.pop("dbpath")
else:
- # Command line options override the YAML configuration.
- dbpath_prefix = utils.default_if_none(config.DBPATH_PREFIX, dbpath_prefix)
- dbpath_prefix = utils.default_if_none(dbpath_prefix, config.DEFAULT_DBPATH_PREFIX)
- self._dbpath_prefix = os.path.join(dbpath_prefix,
- "job%d" % (self.job_num),
- config.FIXTURE_SUBDIR)
+ self._dbpath_prefix = os.path.join(self._dbpath_prefix, config.FIXTURE_SUBDIR)
self.nodes = []
self.replset_name = None
diff --git a/buildscripts/resmokelib/testing/fixtures/shardedcluster.py b/buildscripts/resmokelib/testing/fixtures/shardedcluster.py
index 2e2db535d6d..9a90c5b31e1 100644
--- a/buildscripts/resmokelib/testing/fixtures/shardedcluster.py
+++ b/buildscripts/resmokelib/testing/fixtures/shardedcluster.py
@@ -47,7 +47,7 @@ class ShardedClusterFixture(interface.Fixture):
the mongod and mongos processes.
"""
- interface.Fixture.__init__(self, logger, job_num)
+ interface.Fixture.__init__(self, logger, job_num, dbpath_prefix=dbpath_prefix)
if "dbpath" in mongod_options:
raise ValueError("Cannot specify mongod_options.dbpath")
@@ -62,12 +62,7 @@ class ShardedClusterFixture(interface.Fixture):
self.enable_sharding = utils.default_if_none(enable_sharding, [])
self.auth_options = auth_options
- # Command line options override the YAML configuration.
- dbpath_prefix = utils.default_if_none(config.DBPATH_PREFIX, dbpath_prefix)
- dbpath_prefix = utils.default_if_none(dbpath_prefix, config.DEFAULT_DBPATH_PREFIX)
- self._dbpath_prefix = os.path.join(dbpath_prefix,
- "job%d" % (self.job_num),
- config.FIXTURE_SUBDIR)
+ self._dbpath_prefix = os.path.join(self._dbpath_prefix, config.FIXTURE_SUBDIR)
self.configsvr = None
self.mongos = None
@@ -108,11 +103,7 @@ class ShardedClusterFixture(interface.Fixture):
self.port = self.mongos.port
client = utils.new_mongo_client(port=self.port)
- if self.auth_options is not None:
- auth_db = client[self.auth_options["authenticationDatabase"]]
- auth_db.authenticate(self.auth_options["username"],
- password=self.auth_options["password"],
- mechanism=self.auth_options["authenticationMechanism"])
+ self._auth_to_db(client)
# Inform mongos about each of the shards
for shard in self.shards:
@@ -123,6 +114,20 @@ class ShardedClusterFixture(interface.Fixture):
self.logger.info("Enabling sharding for '%s' database...", db_name)
client.admin.command({"enablesharding": db_name})
+ def _auth_to_db(self, client):
+ """Authenticate client for the 'authenticationDatabase'."""
+ if self.auth_options is not None:
+ auth_db = client[self.auth_options["authenticationDatabase"]]
+ auth_db.authenticate(self.auth_options["username"],
+ password=self.auth_options["password"],
+ mechanism=self.auth_options["authenticationMechanism"])
+
+ def _stop_balancer(self, timeout_ms=60000):
+ """Stop the balancer."""
+ client = utils.new_mongo_client(port=self.port)
+ self._auth_to_db(client)
+ client.admin.command({"balancerStop": 1}, maxTimeMS=timeout_ms)
+
def _do_teardown(self):
"""
Shuts down the sharded cluster.
@@ -134,6 +139,8 @@ class ShardedClusterFixture(interface.Fixture):
self.logger.info(
"Sharded cluster was expected to be running in _do_teardown(), but wasn't.")
+ self._stop_balancer()
+
if self.configsvr is not None:
if running_at_start:
self.logger.info("Stopping config server...")
@@ -154,6 +161,8 @@ class ShardedClusterFixture(interface.Fixture):
if running_at_start:
self.logger.info("Stopping shards...")
+
+
for shard in self.shards:
success = shard.teardown() and success
if running_at_start:
diff --git a/buildscripts/resmokelib/testing/fixtures/standalone.py b/buildscripts/resmokelib/testing/fixtures/standalone.py
index bc69775c285..d462685a827 100644
--- a/buildscripts/resmokelib/testing/fixtures/standalone.py
+++ b/buildscripts/resmokelib/testing/fixtures/standalone.py
@@ -6,7 +6,6 @@ from __future__ import absolute_import
import os
import os.path
-import shutil
import time
import pymongo
@@ -34,7 +33,7 @@ class MongoDFixture(interface.Fixture):
dbpath_prefix=None,
preserve_dbpath=False):
- interface.Fixture.__init__(self, logger, job_num)
+ interface.Fixture.__init__(self, logger, job_num, dbpath_prefix=dbpath_prefix)
if "dbpath" in mongod_options and dbpath_prefix is not None:
raise ValueError("Cannot specify both mongod_options.dbpath and dbpath_prefix")
@@ -48,19 +47,16 @@ class MongoDFixture(interface.Fixture):
# The dbpath in mongod_options takes precedence over other settings to make it easier for
# users to specify a dbpath containing data to test against.
if "dbpath" not in self.mongod_options:
- # Command line options override the YAML configuration.
- dbpath_prefix = utils.default_if_none(config.DBPATH_PREFIX, dbpath_prefix)
- dbpath_prefix = utils.default_if_none(dbpath_prefix, config.DEFAULT_DBPATH_PREFIX)
- self.mongod_options["dbpath"] = os.path.join(dbpath_prefix,
- "job%d" % (self.job_num),
- config.FIXTURE_SUBDIR)
+ self.mongod_options["dbpath"] = os.path.join(
+ self._dbpath_prefix, config.FIXTURE_SUBDIR)
self._dbpath = self.mongod_options["dbpath"]
self.mongod = None
def setup(self):
- if not self.preserve_dbpath:
- shutil.rmtree(self._dbpath, ignore_errors=True)
+ """Set up the mongod."""
+ if not self.preserve_dbpath and os.path.lexists(self._dbpath):
+ utils.rmtree(self._dbpath, ignore_errors=False)
try:
os.makedirs(self._dbpath)
@@ -145,6 +141,10 @@ class MongoDFixture(interface.Fixture):
def is_running(self):
return self.mongod is not None and self.mongod.poll() is None
+ def get_dbpath_prefix(self):
+ """ Returns the _dbpath, as this is the root of the data directory. """
+ return self._dbpath
+
def get_internal_connection_string(self):
if self.mongod is None:
raise ValueError("Must call setup() before calling get_internal_connection_string()")
diff --git a/buildscripts/resmokelib/testing/hook_test_archival.py b/buildscripts/resmokelib/testing/hook_test_archival.py
new file mode 100644
index 00000000000..bde562cf617
--- /dev/null
+++ b/buildscripts/resmokelib/testing/hook_test_archival.py
@@ -0,0 +1,116 @@
+"""
+Enables supports for archiving tests or hooks.
+"""
+
+from __future__ import absolute_import
+
+import os
+import threading
+
+from .. import config
+from .. import utils
+from ..utils import globstar
+
+
+class HookTestArchival(object):
+ """
+ Archives hooks and tests to S3.
+ """
+
+ def __init__(self, suite, hooks, archive_instance, archive_config):
+ self.archive_instance = archive_instance
+ archive_config = utils.default_if_none(archive_config, {})
+
+ self.on_success = archive_config.get("on_success", False)
+
+ self.tests = []
+ if "tests" in archive_config:
+ # 'tests' is either a list of tests to archive or a bool (archive all if True).
+ if not isinstance(archive_config["tests"], bool):
+ for test in archive_config["tests"]:
+ self.tests += globstar.glob(test)
+ elif archive_config["tests"]:
+ self.tests = suite.tests
+
+ self.hooks = []
+ if "hooks" in archive_config:
+ # 'hooks' is either a list of hooks to archive or a bool (archive all if True).
+ if not isinstance(archive_config["hooks"], bool):
+ self.hooks = archive_config["hooks"]
+ elif archive_config["hooks"]:
+ for hook in hooks:
+ self.hooks.append(hook["class"])
+
+ self._tests_repeat = {}
+ self._lock = threading.Lock()
+
+ def _should_archive(self, success):
+ """ Return True if failed test or 'on_success' is True. """
+ return not success or self.on_success
+
+ def _archive_hook(self, logger, hook, test, success):
+ """ Helper to archive hooks. """
+ hook_match = hook.__class__.__name__ in self.hooks
+ if not hook_match or not self._should_archive(success):
+ return
+
+ test_name = "{}:{}".format(test.short_name(), hook.__class__.__name__)
+ self._archive_hook_or_test(logger, test_name, test)
+
+ def _archive_test(self, logger, test, success):
+ """ Helper to archive tests. """
+ test_name = test.test_name
+ test_match = False
+ for arch_test in self.tests:
+ # Ensure that the test_name is in the same format as the arch_test.
+ if os.path.normpath(test_name) == os.path.normpath(arch_test):
+ test_match = True
+ break
+ if not test_match or not self._should_archive(success):
+ return
+
+ self._archive_hook_or_test(logger, test_name, test)
+
+ def archive(self, logger, test, success, hook=None):
+ """ Archives data files for hooks or tests. """
+ if not config.ARCHIVE_FILE or not self.archive_instance:
+ return
+ if hook:
+ self._archive_hook(logger, hook, test, success)
+ else:
+ self._archive_test(logger, test, success)
+
+ def _archive_hook_or_test(self, logger, test_name, test):
+ """ Trigger archive of data files for a test or hook. """
+
+ with self._lock:
+ # Test repeat number is how many times the particular test has been archived.
+ if test_name not in self._tests_repeat:
+ self._tests_repeat[test_name] = 0
+ else:
+ self._tests_repeat[test_name] += 1
+ # Normalize test path from a test or hook name.
+ test_path = \
+ test_name.replace("/", "_").replace("\\", "_").replace(".", "_").replace(":", "_")
+ file_name = "mongo-data-{}-{}-{}-{}.tgz".format(
+ config.EVERGREEN_TASK_ID,
+ test_path,
+ config.EVERGREEN_EXECUTION,
+ self._tests_repeat[test_name])
+ # Retrieve root directory for all dbPaths from fixture.
+ input_files = test.fixture.get_dbpath_prefix()
+ s3_bucket = config.ARCHIVE_BUCKET
+ s3_path = "{}/{}/{}/datafiles/{}".format(
+ config.EVERGREEN_PROJECT_NAME,
+ config.EVERGREEN_VARIANT_NAME,
+ config.EVERGREEN_REVISION,
+ file_name)
+ display_name = "Data files {} - Execution {} Repetition {}".format(
+ test_name,
+ config.EVERGREEN_EXECUTION,
+ self._tests_repeat[test_name])
+ logger.info("Archiving data files for test %s from %s", test_name, input_files)
+ status, message = self.archive_instance.archive_files_to_s3(
+ display_name, input_files, s3_bucket, s3_path)
+ if status:
+ logger.warning("Archive failed for %s: %s", test_name, message)
diff --git a/buildscripts/resmokelib/testing/job.py b/buildscripts/resmokelib/testing/job.py
index 4d17152b17d..c8be906dd3a 100644
--- a/buildscripts/resmokelib/testing/job.py
+++ b/buildscripts/resmokelib/testing/job.py
@@ -18,7 +18,7 @@ class Job(object):
Runs tests from a queue.
"""
- def __init__(self, logger, fixture, hooks, report):
+ def __init__(self, logger, fixture, hooks, report, archival):
"""
Initializes the job with the specified fixture and custom
behaviors.
@@ -28,6 +28,7 @@ class Job(object):
self.fixture = fixture
self.hooks = hooks
self.report = report
+ self.archival = archival
def __call__(self, queue, interrupt_flag, teardown_flag=None):
"""
@@ -98,20 +99,38 @@ class Job(object):
self._run_hooks_before_tests(test)
test(self.report)
- if config.FAIL_FAST and not self.report.wasSuccessful():
- self.logger.info("%s failed, so stopping..." % (test.shortDescription()))
- raise errors.StopExecution("%s failed" % (test.shortDescription()))
-
- if not self.fixture.is_running():
- self.logger.error("%s marked as a failure because the fixture crashed during the test.",
- test.shortDescription())
- self.report.setFailure(test, return_code=2)
- # Always fail fast if the fixture fails.
- raise errors.StopExecution("%s not running after %s" %
- (self.fixture, test.shortDescription()))
+ try:
+ if config.FAIL_FAST and not self.report.wasSuccessful():
+ self.logger.info("%s failed, so stopping..." % (test.shortDescription()))
+ raise errors.StopExecution("%s failed" % (test.shortDescription()))
+
+ if not self.fixture.is_running():
+ self.logger.error(
+ "%s marked as a failure because the fixture crashed during the test.",
+ test.shortDescription())
+ self.report.setFailure(test, return_code=2)
+ # Always fail fast if the fixture fails.
+ raise errors.StopExecution("%s not running after %s" %
+ (self.fixture, test.shortDescription()))
+
+ finally:
+ success = self.report.find_test_info(test).status == "pass"
+ self.archival.archive(self.logger, test, success)
+ if self.archival:
+ self.archival.archive(self.logger, test, success)
self._run_hooks_after_tests(test)
+ def _run_hook(self, hook, hook_function, test):
+ """Provide helper to run hook and archival."""
+ try:
+ success = False
+ hook_function(test, self.report)
+ success = True
+ finally:
+ if self.archival:
+ self.archival.archive(self.logger, test, success, hook=hook)
+
def _run_hooks_before_tests(self, test):
"""
Runs the before_test method on each of the hooks.
@@ -122,7 +141,7 @@ class Job(object):
try:
for hook in self.hooks:
- hook.before_test(test, self.report)
+ self._run_hook(hook, hook.before_test, test)
except errors.StopExecution:
raise
@@ -156,7 +175,7 @@ class Job(object):
"""
try:
for hook in self.hooks:
- hook.after_test(test, self.report)
+ self._run_hook(hook, hook.after_test, test)
except errors.StopExecution:
raise
diff --git a/buildscripts/resmokelib/testing/report.py b/buildscripts/resmokelib/testing/report.py
index b9cccccc14f..e3c68393a4d 100644
--- a/buildscripts/resmokelib/testing/report.py
+++ b/buildscripts/resmokelib/testing/report.py
@@ -147,7 +147,7 @@ class TestReport(unittest.TestResult):
unittest.TestResult.stopTest(self, test)
with self._lock:
- test_info = self._find_test_info(test)
+ test_info = self.find_test_info(test)
test_info.end_time = time.time()
time_taken = test_info.end_time - test_info.start_time
@@ -177,7 +177,7 @@ class TestReport(unittest.TestResult):
with self._lock:
self.num_errored += 1
- test_info = self._find_test_info(test)
+ test_info = self.find_test_info(test)
test_info.status = "error"
test_info.return_code = test.return_code
@@ -187,7 +187,7 @@ class TestReport(unittest.TestResult):
"""
with self._lock:
- test_info = self._find_test_info(test)
+ test_info = self.find_test_info(test)
if test_info.end_time is None:
raise ValueError("stopTest was not called on %s" % (test.basename()))
@@ -211,7 +211,7 @@ class TestReport(unittest.TestResult):
with self._lock:
self.num_failed += 1
- test_info = self._find_test_info(test)
+ test_info = self.find_test_info(test)
test_info.status = "fail"
test_info.return_code = test.return_code
@@ -221,7 +221,7 @@ class TestReport(unittest.TestResult):
"""
with self._lock:
- test_info = self._find_test_info(test)
+ test_info = self.find_test_info(test)
if test_info.end_time is None:
raise ValueError("stopTest was not called on %s" % (test.basename()))
@@ -244,7 +244,7 @@ class TestReport(unittest.TestResult):
with self._lock:
self.num_succeeded += 1
- test_info = self._find_test_info(test)
+ test_info = self.find_test_info(test)
test_info.status = "pass"
test_info.return_code = test.return_code
@@ -352,7 +352,7 @@ class TestReport(unittest.TestResult):
# protecting it with the lock.
self.__original_loggers = {}
- def _find_test_info(self, test):
+ def find_test_info(self, test):
"""
Returns the status and timing information associated with
'test'.
diff --git a/buildscripts/resmokelib/testing/testcases.py b/buildscripts/resmokelib/testing/testcases.py
index 21d35215a29..78110ff680d 100644
--- a/buildscripts/resmokelib/testing/testcases.py
+++ b/buildscripts/resmokelib/testing/testcases.py
@@ -6,7 +6,6 @@ from __future__ import absolute_import
import os
import os.path
-import shutil
import threading
import unittest
@@ -236,7 +235,7 @@ class DBTestCase(TestCase):
dbpath = os.path.join(dbpath_prefix, "job%d" % (self.fixture.job_num), "unittest")
self.dbtest_options["dbpath"] = dbpath
- shutil.rmtree(dbpath, ignore_errors=True)
+ utils.rmtree(dbpath, ignore_errors=True)
try:
os.makedirs(dbpath)
@@ -352,7 +351,7 @@ class JSTestCase(TestCase):
global_vars["TestData"] = test_data
self.shell_options["global_vars"] = global_vars
- shutil.rmtree(data_dir, ignore_errors=True)
+ utils.rmtree(data_dir, ignore_errors=True)
self.num_clients = num_clients
@@ -364,14 +363,19 @@ class JSTestCase(TestCase):
process_kwargs = self.shell_options.get("process_kwargs", {}).copy()
- if "KRB5_CONFIG" in process_kwargs and "KRB5CCNAME" not in process_kwargs:
+ if process_kwargs \
+ and "env_vars" in process_kwargs \
+ and "KRB5_CONFIG" in process_kwargs["env_vars"] \
+ and "KRB5CCNAME" not in process_kwargs["env_vars"]:
# Use a job-specific credential cache for JavaScript tests involving Kerberos.
krb5_dir = os.path.join(data_dir, "krb5")
+
try:
os.makedirs(krb5_dir)
except os.error:
pass
- process_kwargs["KRB5CCNAME"] = "DIR:" + os.path.join(krb5_dir, ".")
+
+ process_kwargs["env_vars"]["KRB5CCNAME"] = "DIR:" + krb5_dir
self.shell_options["process_kwargs"] = process_kwargs
diff --git a/buildscripts/resmokelib/utils/__init__.py b/buildscripts/resmokelib/utils/__init__.py
index df387cc3323..ff37fbb954a 100644
--- a/buildscripts/resmokelib/utils/__init__.py
+++ b/buildscripts/resmokelib/utils/__init__.py
@@ -5,15 +5,41 @@ Helper functions.
from __future__ import absolute_import
import os.path
+import shutil
+import sys
import pymongo
import yaml
+from . import archival
+
def default_if_none(value, default):
return value if value is not None else default
+def rmtree(path, **kwargs):
+ """Wrap shutil.rmtreee.
+
+ Use a UTF-8 unicode path if Windows.
+ See https://bugs.python.org/issue24672, where shutil.rmtree can fail with UTF-8.
+ Use a bytes path to rmtree, otherwise.
+ See https://github.com/pypa/setuptools/issues/706.
+ """
+ if is_windows():
+ if not isinstance(path, unicode):
+ path = unicode(path, "utf-8")
+ else:
+ if isinstance(path, unicode):
+ path = path.encode("utf-8")
+ shutil.rmtree(path, **kwargs)
+
+
+def is_windows():
+ """Return True if Windows."""
+ return sys.platform.startswith("win32") or sys.platform.startswith("cygwin")
+
+
def is_string_list(lst):
"""
Returns true if 'lst' is a list of strings, and false otherwise.
diff --git a/buildscripts/resmokelib/utils/archival.py b/buildscripts/resmokelib/utils/archival.py
new file mode 100644
index 00000000000..1002f2ce902
--- /dev/null
+++ b/buildscripts/resmokelib/utils/archival.py
@@ -0,0 +1,331 @@
+"""
+Archival utility.
+"""
+
+from __future__ import absolute_import
+
+import Queue
+import collections
+import json
+import math
+import os
+import sys
+import tarfile
+import tempfile
+import threading
+import time
+
+_IS_WINDOWS = sys.platform == "win32" or sys.platform == "cygwin"
+
+if _IS_WINDOWS:
+ import ctypes
+
+UploadArgs = collections.namedtuple(
+ "UploadArgs",
+ ["archival_file",
+ "display_name",
+ "local_file",
+ "content_type",
+ "s3_bucket",
+ "s3_path",
+ "delete_file"])
+
+ArchiveArgs = collections.namedtuple(
+ "ArchiveArgs", ["archival_file", "display_name", "remote_file"])
+
+
+def file_list_size(files):
+ """ Return size (in bytes) of all 'files' and their subdirectories. """
+ if isinstance(files, str):
+ files = [files]
+ file_bytes = 0
+ for ifile in files:
+ if not os.path.exists(ifile):
+ pass
+ elif os.path.isdir(ifile):
+ file_bytes += directory_size(ifile)
+ else:
+ file_bytes += os.path.getsize(ifile)
+ return file_bytes
+
+
+def directory_size(directory):
+ """ Return size (in bytes) of files in 'directory' tree. """
+ dir_bytes = 0
+ for root_dir, _, files in os.walk(unicode(directory)):
+ for name in files:
+ full_name = os.path.join(root_dir, name)
+ try:
+ dir_bytes += os.path.getsize(full_name)
+ except OSError:
+ # A file might be deleted while we are looping through the os.walk() result.
+ pass
+ return dir_bytes
+
+
+def free_space(path):
+ """ Return file system free space (in bytes) for 'path'. """
+ if _IS_WINDOWS:
+ dirname = os.path.dirname(path)
+ free_bytes = ctypes.c_ulonglong(0)
+ ctypes.windll.kernel32.GetDiskFreeSpaceExW(
+ ctypes.c_wchar_p(dirname), None, None, ctypes.pointer(free_bytes))
+ return free_bytes.value
+ else:
+ stat = os.statvfs(path)
+ return stat.f_bavail * stat.f_bsize
+
+
+def remove_file(file_name):
+ """ Attempts to remove file. Returns status and message. """
+ try:
+ # File descriptors, on Windows, are inherited by all subprocesses and file removal may fail
+ # because the file is still open.
+ # See https://www.python.org/dev/peps/pep-0446/#issues-with-inheritable-file-descriptors
+ os.remove(file_name)
+ status = 0
+ message = "Successfully deleted file {}".format(file_name)
+ except Exception as err:
+ status = 1
+ message = "Error deleting file {}: {}".format(file_name, err)
+ return status, message
+
+
+class Archival(object):
+ """ Class to support file archival to S3."""
+
+ def __init__(self,
+ logger,
+ archival_json_file="archive.json",
+ limit_size_mb=0,
+ limit_files=0,
+ s3_client=None):
+ """ Archival init method. """
+
+ self.archival_json_file = archival_json_file
+ self.limit_size_mb = limit_size_mb
+ self.limit_files = limit_files
+ self.size_mb = 0
+ self.num_files = 0
+ self.archive_time = 0
+ self.logger = logger
+
+ # Lock to control access from multiple threads.
+ self._lock = threading.Lock()
+
+ # Start the worker thread to update the 'archival_json_file'.
+ self._archive_file_queue = Queue.Queue()
+ self._archive_file_worker = threading.Thread(
+ target=self._update_archive_file_wkr,
+ args=(self._archive_file_queue, logger),
+ name="archive_file_worker")
+ self._archive_file_worker.setDaemon(True)
+ self._archive_file_worker.start()
+ if not s3_client:
+ self.s3_client = self._get_s3_client()
+ else:
+ self.s3_client = s3_client
+
+ # Start the worker thread which uploads the archive.
+ self._upload_queue = Queue.Queue()
+ self._upload_worker = threading.Thread(
+ target=self._upload_to_s3_wkr,
+ args=(self._upload_queue, self._archive_file_queue, logger, self.s3_client),
+ name="upload_worker")
+ self._upload_worker.setDaemon(True)
+ self._upload_worker.start()
+
+ @staticmethod
+ def _get_s3_client():
+ # Since boto3 is a 3rd party module, we import locally.
+ import boto3
+ return boto3.client("s3")
+
+ def archive_files_to_s3(self, display_name, input_files, s3_bucket, s3_path):
+ """
+ Archive 'input_files' to 's3_bucket' and 's3_path'.
+
+ Archive is not done if user specified limits are reached. The size limit is
+ enforced after it has been exceeded, since it can only be calculated after the
+ tar/gzip has been done.
+
+ Returns status and message, where message contains information if status is non-0.
+ """
+
+ start_time = time.time()
+ with self._lock:
+ if not input_files:
+ status = 1
+ message = "No input_files specified"
+ elif self.limit_size_mb and self.size_mb >= self.limit_size_mb:
+ status = 1
+ message = "Files not archived, {}MB size limit reached".format(self.limit_size_mb)
+ elif self.limit_files and self.num_files >= self.limit_files:
+ status = 1
+ message = "Files not archived, {} file limit reached".format(self.limit_files)
+ else:
+ status, message, file_size_mb = self._archive_files(
+ display_name,
+ input_files,
+ s3_bucket,
+ s3_path)
+
+ if status == 0:
+ self.num_files += 1
+ self.size_mb += file_size_mb
+ self.archive_time += time.time() - start_time
+
+ return status, message
+
+ @staticmethod
+ def _update_archive_file_wkr(queue, logger):
+ """ Worker thread: Update the archival JSON file from 'queue'. """
+ archival_json = []
+ while True:
+ archive_args = queue.get()
+ # Exit worker thread when sentinel is received.
+ if archive_args is None:
+ queue.task_done()
+ break
+ archival_record = {
+ "name": archive_args.display_name,
+ "link": archive_args.remote_file,
+ "visibility": "private"
+ }
+ logger.debug(
+ "Updating archive file %s with %s", archive_args.archival_file, archival_record)
+ archival_json.append(archival_record)
+ with open(archive_args.archival_file, "w") as archival_fh:
+ json.dump(archival_json, archival_fh)
+ queue.task_done()
+
+ @staticmethod
+ def _upload_to_s3_wkr(queue, archive_file_queue, logger, s3_client):
+ """" Worker thread: Upload to S3 from 'queue', dispatch to 'archive_file_queue'. """
+ while True:
+ upload_args = queue.get()
+ # Exit worker thread when sentinel is received.
+ if upload_args is None:
+ queue.task_done()
+ archive_file_queue.put(None)
+ break
+ extra_args = {"ContentType": upload_args.content_type, "ACL": "public-read"}
+ logger.debug("Uploading to S3 %s to bucket %s path %s",
+ upload_args.local_file,
+ upload_args.s3_bucket,
+ upload_args.s3_path)
+ upload_completed = False
+ try:
+ s3_client.upload_file(upload_args.local_file,
+ upload_args.s3_bucket,
+ upload_args.s3_path,
+ ExtraArgs=extra_args)
+ upload_completed = True
+ logger.debug("Upload to S3 completed for %s to bucket %s path %s",
+ upload_args.local_file,
+ upload_args.s3_bucket,
+ upload_args.s3_path)
+ except Exception as err:
+ logger.exception("Upload to S3 error %s", err)
+
+ if upload_args.delete_file:
+ status, message = remove_file(upload_args.local_file)
+ if status:
+ logger.error("Upload to S3 delete file error %s", message)
+
+ remote_file = "https://s3.amazonaws.com/{}/{}".format(
+ upload_args.s3_bucket, upload_args.s3_path)
+ if upload_completed:
+ archive_file_queue.put(ArchiveArgs(
+ upload_args.archival_file, upload_args.display_name, remote_file))
+
+ queue.task_done()
+
+ def _archive_files(self, display_name, input_files, s3_bucket, s3_path):
+ """
+ Gather 'input_files' into a single tar/gzip and archive to 's3_path'.
+
+ The caller waits until the list of files has been tar/gzipped to a temporary file.
+ The S3 upload and subsequent update to 'archival_json_file' will be done asynchronosly.
+
+ Returns status, message and size_mb of archive.
+ """
+
+ # Parameter 'input_files' can either be a string or list of strings.
+ if isinstance(input_files, str):
+ input_files = [input_files]
+
+ message = "Tar/gzip {} files: {}".format(display_name, input_files)
+ status = 0
+ size_mb = 0
+
+ # Tar/gzip to a temporary file.
+ _, temp_file = tempfile.mkstemp(suffix=".tgz")
+
+ # Check if there is sufficient space for the temporary tgz file.
+ if file_list_size(input_files) > free_space(temp_file):
+ status, message = remove_file(temp_file)
+ if status:
+ self.logger.warning("Removing tarfile due to insufficient space - %s", message)
+ return 1, "Insufficient space for {}".format(message), 0
+
+ try:
+ with tarfile.open(temp_file, "w:gz") as tar_handle:
+ for input_file in input_files:
+ try:
+ tar_handle.add(input_file)
+ except (IOError, OSError, tarfile.TarError) as err:
+ message = "{}; Unable to add {} to archive file: {}".format(
+ message, input_file, err)
+ except (IOError, OSError, tarfile.TarError) as err:
+ status, message = remove_file(temp_file)
+ if status:
+ self.logger.warning("Removing tarfile due to creation failure - %s", message)
+ return 1, str(err), 0
+
+ # Round up the size of the archive.
+ size_mb = int(math.ceil(float(file_list_size(temp_file)) / (1024 * 1024)))
+ self._upload_queue.put(UploadArgs(
+ self.archival_json_file,
+ display_name,
+ temp_file,
+ "application/x-gzip",
+ s3_bucket,
+ s3_path,
+ True))
+
+ return status, message, size_mb
+
+ def check_thread(self, thread, expected_alive):
+ if thread.isAlive() and not expected_alive:
+ self.logger.warning(
+ "The %s thread did not complete, some files might not have been uploaded"
+ " to S3 or archived to %s.", thread.name, self.archival_json_file)
+ elif not thread.isAlive() and expected_alive:
+ self.logger.warning(
+ "The %s thread is no longer running, some files might not have been uploaded"
+ " to S3 or archived to %s.", thread.name, self.archival_json_file)
+
+ def exit(self, timeout=30):
+ """ Waits for worker threads to finish. """
+ # Put sentinel on upload queue to trigger worker thread exit.
+ self._upload_queue.put(None)
+ self.check_thread(self._upload_worker, True)
+ self.check_thread(self._archive_file_worker, True)
+ self._upload_worker.join(timeout=timeout)
+ self.check_thread(self._upload_worker, False)
+
+ # Archive file worker thread exit should be triggered by upload thread worker.
+ self._archive_file_worker.join(timeout=timeout)
+ self.check_thread(self._archive_file_worker, False)
+
+ self.logger.info("Total tar/gzip archive time is %0.2f seconds, for %d file(s) %d MB",
+ self.archive_time, self.num_files, self.size_mb)
+
+ def files_archived_num(self):
+ """ Returns the number of the archived files. """
+ return self.num_files
+
+ def files_archived_size_mb(self):
+ """ Returns the size of the archived files. """
+ return self.size_mb
diff --git a/buildscripts/tests/resmokelib/test_archival.py b/buildscripts/tests/resmokelib/test_archival.py
new file mode 100644
index 00000000000..bc36cdb4832
--- /dev/null
+++ b/buildscripts/tests/resmokelib/test_archival.py
@@ -0,0 +1,251 @@
+#!/usr/bin/env python
+
+""" Unit tests for archival. """
+
+import logging
+import os
+import random
+import shutil
+import sys
+import tempfile
+import unittest
+
+
+if __name__ == "__main__" and __package__ is None:
+ sys.path.append(os.getcwd())
+from buildscripts.resmokelib.utils import archival
+
+_BUCKET = "mongodatafiles"
+
+
+def create_random_file(file_name, num_chars_mb):
+ """ Creates file with random characters, which will have minimal compression. """
+ with open(file_name, "wb") as fileh:
+ for _ in xrange(num_chars_mb * 1024 * 1024):
+ fileh.write(chr(random.randint(0, 255)))
+
+
+class MockS3Client(object):
+ """ Class to mock the S3 client. """
+
+ def __init__(self, logger):
+ self.logger = logger
+ self.logger.info("MockS3Client init")
+ return
+
+ def upload_file(self, *args, **kwargs):
+ self.logger.info("MockS3Client upload_file %s %s", args, kwargs)
+ return
+
+ def delete_object(self, *args, **kwargs):
+ self.logger.info("MockS3Client delete_object %s %s", args, kwargs)
+ return
+
+
+class ArchivalTestCase(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ logging.basicConfig()
+ cls.logger = logging.getLogger()
+ cls.logger.setLevel(logging.INFO)
+ cls.bucket = _BUCKET
+ cls.temp_dir = tempfile.mkdtemp()
+ cls.remote_paths = []
+
+ # We can use the AWS S3 calls by setting the environment variable MOCK_CLIENT to 0.
+ mock_client = os.environ.get("MOCK_CLIENT", "1") != "0"
+ if mock_client:
+ cls.s3_client = MockS3Client(cls.logger)
+ else:
+ cls.s3_client = archival.Archival._get_s3_client()
+ cls.archive = cls.create_archival()
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.archive.exit()
+ cls.logger.info("Cleaning temp directory %s", cls.temp_dir)
+ shutil.rmtree(cls.temp_dir, ignore_errors=True)
+ for path in cls.remote_paths:
+ cls.delete_s3_file(path)
+
+ @classmethod
+ def create_archival(cls):
+ return archival.Archival(cls.logger, s3_client=cls.s3_client)
+
+ @classmethod
+ def delete_s3_file(cls, path):
+ cls.logger.info("Cleaning S3 bucket %s path %s", cls.bucket, path)
+ cls.s3_client.delete_object(Bucket=cls.bucket, Key=path)
+
+ def s3_path(self, path, add_remote_path=True):
+ if add_remote_path:
+ self.remote_paths.append(path)
+ return path
+
+
+class ArchivalFileTests(ArchivalTestCase):
+ def test_nofile(self):
+ # Invalid input_files
+ display_name = "Unittest invalid file"
+ input_files = "no_file"
+ s3_path = self.s3_path("unittest/no_file.tgz", False)
+ self.assertRaises(
+ OSError,
+ lambda: self.archive.archive_files_to_s3(
+ display_name, input_files, self.bucket, s3_path))
+
+ # Invalid input_files in a list
+ input_files = ["no_file", "no_file2"]
+ s3_path = self.s3_path("unittest/no_files.tgz", False)
+ self.assertRaises(
+ OSError,
+ lambda: self.archive.archive_files_to_s3(
+ display_name, input_files, self.bucket, s3_path))
+
+ # No files
+ display_name = "Unittest no files"
+ s3_path = self.s3_path("unittest/no_files.tgz")
+ status, message = self.archive.archive_files_to_s3(
+ display_name, [], self.bucket, s3_path)
+ self.assertEqual(1, status, message)
+
+ def test_files(self):
+ # Valid files
+ display_name = "Unittest valid file"
+ temp_file = tempfile.mkstemp(dir=self.temp_dir)[1]
+ s3_path = self.s3_path("unittest/valid_file.tgz")
+ status, message = self.archive.archive_files_to_s3(
+ display_name, temp_file, self.bucket, s3_path)
+ self.assertEqual(0, status, message)
+
+ # 2 valid files
+ display_name = "Unittest 2 valid files"
+ temp_file2 = tempfile.mkstemp(dir=self.temp_dir)[1]
+ s3_path = self.s3_path("unittest/2valid_files.tgz")
+ status, message = self.archive.archive_files_to_s3(
+ display_name, [temp_file, temp_file2], self.bucket, s3_path)
+ self.assertEqual(0, status, message)
+
+ def test_empty_directory(self):
+ # Valid directory
+ display_name = "Unittest valid directory no files"
+ temp_dir = tempfile.mkdtemp(dir=self.temp_dir)
+ s3_path = self.s3_path("unittest/valid_directory.tgz")
+ status, message = self.archive.archive_files_to_s3(
+ display_name, temp_dir, self.bucket, s3_path)
+ self.assertEqual(0, status, message)
+
+ display_name = "Unittest valid directories no files"
+ temp_dir2 = tempfile.mkdtemp(dir=self.temp_dir)
+ s3_path = self.s3_path("unittest/valid_directories.tgz")
+ status, message = self.archive.archive_files_to_s3(
+ display_name, [temp_dir, temp_dir2], self.bucket, s3_path)
+ self.assertEqual(0, status, message)
+
+ def test_directory(self):
+ display_name = "Unittest directory with files"
+ temp_dir = tempfile.mkdtemp(dir=self.temp_dir)
+ s3_path = self.s3_path("unittest/directory_with_files.tgz")
+ # Create 10 empty files
+ for _ in xrange(10):
+ tempfile.mkstemp(dir=temp_dir)
+ status, message = self.archive.archive_files_to_s3(
+ display_name, temp_dir, self.bucket, s3_path)
+ self.assertEqual(0, status, message)
+
+ display_name = "Unittest 2 valid directory files"
+ temp_dir2 = tempfile.mkdtemp(dir=self.temp_dir)
+ s3_path = self.s3_path("unittest/directories_with_files.tgz")
+ # Create 10 empty files
+ for _ in xrange(10):
+ tempfile.mkstemp(dir=temp_dir2)
+ status, message = self.archive.archive_files_to_s3(
+ display_name, [temp_dir, temp_dir2], self.bucket, s3_path)
+ self.assertEqual(0, status, message)
+
+
+class ArchivalLimitSizeTests(ArchivalTestCase):
+ @classmethod
+ def create_archival(cls):
+ return archival.Archival(cls.logger, limit_size_mb=5, s3_client=cls.s3_client)
+
+ def test_limit_size(self):
+
+ # Files within limit size
+ display_name = "Unittest under limit size"
+ temp_file = tempfile.mkstemp(dir=self.temp_dir)[1]
+ create_random_file(temp_file, 3)
+ s3_path = self.s3_path("unittest/valid_limit_size.tgz")
+ status, message = self.archive.archive_files_to_s3(
+ display_name, temp_file, self.bucket, s3_path)
+ self.assertEqual(0, status, message)
+ # Note the size limit is enforced after the file uploaded. Subsequent
+ # uploads will not be permitted, once the limit has been reached.
+ status, message = self.archive.archive_files_to_s3(
+ display_name, temp_file, self.bucket, s3_path)
+ self.assertEqual(0, status, message)
+
+ # Files beyond limit size
+ display_name = "Unittest over limit size"
+ status, message = self.archive.archive_files_to_s3(
+ display_name, temp_file, self.bucket, s3_path)
+ self.assertEqual(1, status, message)
+
+
+class ArchivalLimitFileTests(ArchivalTestCase):
+ @classmethod
+ def create_archival(cls):
+ return archival.Archival(cls.logger, limit_files=3, s3_client=cls.s3_client)
+
+ def test_limit_file(self):
+
+ # Files within limit number
+ display_name = "Unittest under limit number"
+ temp_file = tempfile.mkstemp(dir=self.temp_dir)[1]
+ s3_path = self.s3_path("unittest/valid_limit_number.tgz")
+ status, message = self.archive.archive_files_to_s3(
+ display_name, temp_file, self.bucket, s3_path)
+ self.assertEqual(0, status, message)
+ status, message = self.archive.archive_files_to_s3(
+ display_name, temp_file, self.bucket, s3_path)
+ self.assertEqual(0, status, message)
+ status, message = self.archive.archive_files_to_s3(
+ display_name, temp_file, self.bucket, s3_path)
+ self.assertEqual(0, status, message)
+
+ # Files beyond limit number
+ display_name = "Unittest over limit number"
+ status, message = self.archive.archive_files_to_s3(
+ display_name, temp_file, self.bucket, s3_path)
+ self.assertEqual(1, status, message)
+
+
+class ArchivalLimitTests(ArchivalTestCase):
+ @classmethod
+ def create_archival(cls):
+ return archival.Archival(
+ cls.logger, limit_size_mb=3, limit_files=3, s3_client=cls.s3_client)
+
+ def test_limits(self):
+
+ # Files within limits
+ display_name = "Unittest under limits"
+ temp_file = tempfile.mkstemp(dir=self.temp_dir)[1]
+ create_random_file(temp_file, 1)
+ s3_path = self.s3_path("unittest/valid_limits.tgz")
+ status, message = self.archive.archive_files_to_s3(
+ display_name, temp_file, self.bucket, s3_path)
+ self.assertEqual(0, status, message)
+ status, message = self.archive.archive_files_to_s3(
+ display_name, temp_file, self.bucket, s3_path)
+ self.assertEqual(0, status, message)
+
+ # Files beyond limits
+ display_name = "Unittest over limits"
+ status, message = self.archive.archive_files_to_s3(
+ display_name, temp_file, self.bucket, s3_path)
+ self.assertEqual(1, status, message)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/buildscripts/tests/resmokelib/utils/__init__.py b/buildscripts/tests/resmokelib/utils/__init__.py
new file mode 100644
index 00000000000..4b7a2bb941b
--- /dev/null
+++ b/buildscripts/tests/resmokelib/utils/__init__.py
@@ -0,0 +1 @@
+"""Empty."""
diff --git a/buildscripts/tests/resmokelib/utils/test_rmtree.py b/buildscripts/tests/resmokelib/utils/test_rmtree.py
new file mode 100644
index 00000000000..1908395766c
--- /dev/null
+++ b/buildscripts/tests/resmokelib/utils/test_rmtree.py
@@ -0,0 +1,126 @@
+# -*- coding: utf-8 -*-
+""" Unit tests for utils.rmtree. """
+
+from __future__ import absolute_import
+from __future__ import print_function
+
+import os
+import shutil
+import sys
+import tempfile
+import unittest
+
+from buildscripts.resmokelib import utils
+
+# pylint: disable=missing-docstring,protected-access
+
+
+def rmtree(dir_root):
+ """Invoke utils.rmtree(dir_root) and return True if removed."""
+ utils.rmtree(dir_root)
+ return not os.path.exists(dir_root)
+
+
+def create_file(path):
+ """Create file named 'path'."""
+ with open(path, "w") as fh:
+ fh.write("")
+
+
+def ascii_filesystemencoding():
+ """Return True if the file system encoding is type ASCII.
+
+ See https://www.iana.org/assignments/character-sets/character-sets.xhtml.
+ """
+ encoding = sys.getfilesystemencoding()
+ return encoding.startswith("ANSI_X3.4") or encoding == "US-ASCII"
+
+
+def string_for_ascii_filesystem_encoding(path):
+ """Return encoded string type for unicode on ASCII file system encoding.
+
+ Some file system encodings are set to ASCII if LANG=C or LC_ALL=C is specified.
+ """
+ if ascii_filesystemencoding() and isinstance(path, unicode):
+ return path.encode("utf-8")
+ return path
+
+
+class RmtreeTestCase(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.temp_dir_root = tempfile.mkdtemp()
+ cls.cwd = os.getcwd()
+
+ @classmethod
+ def tearDownClass(cls):
+ os.chdir(cls.cwd)
+ shutil.rmtree(cls.temp_dir_root, ignore_errors=True)
+
+ def do_test(self, name):
+ pass
+
+ def test_ascii(self):
+ # ASCII name
+ self.do_test("ascii")
+
+ def test_unicode(self):
+ # Unicode name
+ self.do_test(u"unicode")
+
+ def test_greek(self):
+ # Name with Greek
+ self.do_test(string_for_ascii_filesystem_encoding(u"ελληνικά"))
+
+ def test_japanese(self):
+ # Name with Japanese
+ self.do_test(string_for_ascii_filesystem_encoding(u"会社案"))
+
+
+class RmtreeFileTests(RmtreeTestCase):
+ def do_test(self, file_name): # pylint: disable=arguments-differ
+ """Execute file test."""
+ temp_dir = tempfile.mkdtemp(dir=self.temp_dir_root)
+ os.chdir(temp_dir)
+ create_file(file_name)
+ os.chdir(self.temp_dir_root)
+ self.assertTrue(rmtree(temp_dir))
+
+
+class RmtreeDirectoryTests(RmtreeTestCase):
+ def do_test(self, dir_name): # pylint: disable=arguments-differ
+ """Execute directory test."""
+ os.chdir(self.temp_dir_root)
+ os.mkdir(dir_name)
+ self.assertTrue(rmtree(dir_name))
+
+
+class RmtreeDirectoryWithNonAsciiTests(RmtreeTestCase):
+ def do_test(self, name):
+ """Execute directory with non-ASCII file test."""
+ os.chdir(self.temp_dir_root)
+ os.mkdir(name)
+ os.chdir(name)
+ create_file(name)
+ os.chdir(self.temp_dir_root)
+ self.assertTrue(rmtree(name))
+
+
+class ShutilWindowsRmtreeFileTests(RmtreeFileTests):
+ def do_test(self, file_name):
+ """Execute file test that are known to fail in shutil.rmtree."""
+ if not utils.is_windows():
+ print("Skipping ShutilWindowsRmtreeFileTests on non-Windows platforms")
+ return
+ temp_dir = tempfile.mkdtemp(dir=self.temp_dir_root)
+ os.chdir(temp_dir)
+ create_file(file_name)
+ os.chdir(self.temp_dir_root)
+ with self.assertRaises(WindowsError): # pylint: disable=undefined-variable
+ shutil.rmtree(temp_dir)
+
+ def test_ascii(self):
+ pass
+
+ def test_unicode(self):
+ pass