diff --git a/tests/fixtures/migration/new_cassette.json b/tests/fixtures/migration/new_cassette.json new file mode 100644 index 0000000..b0e5dab --- /dev/null +++ b/tests/fixtures/migration/new_cassette.json @@ -0,0 +1,31 @@ +[ + { + "request": { + "body": null, + "method": "GET", + "headers": { + "Accept-Encoding": "gzip, deflate, compress", + "Accept": "*/*", + "User-Agent": "python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0" + }, + "uri" : "http://httpbin.org:80/ip" + }, + "response": { + "status": { + "message": "OK", + "code": 200 + }, + "headers": [ + "Access-Control-Allow-Origin: *\r\n", + "Content-Type: application/json\r\n", + "Date: Mon, 21 Apr 2014 23:13:40 GMT\r\n", + "Server: gunicorn/0.17.4\r\n", + "Content-Length: 32\r\n", + "Connection: keep-alive\r\n" + ], + "body": { + "string": "{\n \"origin\": \"217.122.164.194\"\n}" + } + } + } +] diff --git a/tests/fixtures/migration/new_cassette.yaml b/tests/fixtures/migration/new_cassette.yaml new file mode 100644 index 0000000..bf67623 --- /dev/null +++ b/tests/fixtures/migration/new_cassette.yaml @@ -0,0 +1,15 @@ +- request: !!python/object:vcr.request.Request + body: null + headers: !!python/object/apply:__builtin__.frozenset + - - !!python/tuple [Accept-Encoding, 'gzip, deflate, compress'] + - !!python/tuple [User-Agent, python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0] + - !!python/tuple [Accept, '*/*'] + method: GET + uri: http://httpbin.org:80/ip + response: + body: {string: !!python/unicode "{\n \"origin\": \"217.122.164.194\"\n}"} + headers: [!!python/unicode "Access-Control-Allow-Origin: *\r\n", !!python/unicode "Content-Type: + application/json\r\n", !!python/unicode "Date: Mon, 21 Apr 2014 23:06:09 GMT\r\n", + !!python/unicode "Server: gunicorn/0.17.4\r\n", !!python/unicode "Content-Length: + 32\r\n", !!python/unicode "Connection: keep-alive\r\n"] + status: {code: 200, message: OK} diff --git a/tests/fixtures/migration/not_cassette.txt b/tests/fixtures/migration/not_cassette.txt new file mode 100644 index 0000000..e1d9fc4 --- /dev/null +++ b/tests/fixtures/migration/not_cassette.txt @@ -0,0 +1 @@ +This is not a cassette diff --git a/tests/fixtures/migration/old_cassette.json b/tests/fixtures/migration/old_cassette.json new file mode 100644 index 0000000..22fd9cd --- /dev/null +++ b/tests/fixtures/migration/old_cassette.json @@ -0,0 +1,34 @@ +[ + { + "request": { + "body": null, + "protocol": "http", + "method": "GET", + "headers": { + "Accept-Encoding": "gzip, deflate, compress", + "Accept": "*/*", + "User-Agent": "python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0" + }, + "host": "httpbin.org", + "path": "/ip", + "port": 80 + }, + "response": { + "status": { + "message": "OK", + "code": 200 + }, + "headers": [ + "Access-Control-Allow-Origin: *\r\n", + "Content-Type: application/json\r\n", + "Date: Mon, 21 Apr 2014 23:13:40 GMT\r\n", + "Server: gunicorn/0.17.4\r\n", + "Content-Length: 32\r\n", + "Connection: keep-alive\r\n" + ], + "body": { + "string": "{\n \"origin\": \"217.122.164.194\"\n}" + } + } + } +] diff --git a/tests/fixtures/migration/old_cassette.yaml b/tests/fixtures/migration/old_cassette.yaml new file mode 100644 index 0000000..970823d --- /dev/null +++ b/tests/fixtures/migration/old_cassette.yaml @@ -0,0 +1,18 @@ +- request: !!python/object:vcr.request.Request + body: null + headers: !!python/object/apply:__builtin__.frozenset + - - !!python/tuple [Accept-Encoding, 'gzip, deflate, compress'] + - !!python/tuple [User-Agent, python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0] + - !!python/tuple [Accept, '*/*'] + host: httpbin.org + method: GET + path: /ip + port: 80 + protocol: http + response: + body: {string: !!python/unicode "{\n \"origin\": \"217.122.164.194\"\n}"} + headers: [!!python/unicode "Access-Control-Allow-Origin: *\r\n", !!python/unicode "Content-Type: + application/json\r\n", !!python/unicode "Date: Mon, 21 Apr 2014 23:06:09 GMT\r\n", + !!python/unicode "Server: gunicorn/0.17.4\r\n", !!python/unicode "Content-Length: + 32\r\n", !!python/unicode "Connection: keep-alive\r\n"] + status: {code: 200, message: OK} diff --git a/tests/unit/test_migration.py b/tests/unit/test_migration.py new file mode 100644 index 0000000..b83d6b1 --- /dev/null +++ b/tests/unit/test_migration.py @@ -0,0 +1,36 @@ +import filecmp +import json +import shutil + +import vcr.migration + + +def test_try_migrate_with_json(tmpdir): + cassette = tmpdir.join('cassette').strpath + shutil.copy('tests/fixtures/migration/old_cassette.json', cassette) + assert vcr.migration.try_migrate(cassette) + with open('tests/fixtures/migration/new_cassette.json', 'r') as f: + expected_json = json.load(f) + with open(cassette, 'r') as f: + actual_json = json.load(f) + assert actual_json == expected_json + + +def test_try_migrate_with_yaml(tmpdir): + cassette = tmpdir.join('cassette').strpath + shutil.copy('tests/fixtures/migration/old_cassette.yaml', cassette) + assert vcr.migration.try_migrate(cassette) + assert filecmp.cmp(cassette, 'tests/fixtures/migration/new_cassette.yaml') + + +def test_try_migrate_with_invalid_or_new_cassettes(tmpdir): + cassette = tmpdir.join('cassette').strpath + files = [ + 'tests/fixtures/migration/not_cassette.txt', + 'tests/fixtures/migration/new_cassette.yaml', + 'tests/fixtures/migration/new_cassette.json', + ] + for file_path in files: + shutil.copy(file_path, cassette) + assert not vcr.migration.try_migrate(cassette) + assert filecmp.cmp(cassette, file_path) # shold not change file diff --git a/vcr/migration.py b/vcr/migration.py new file mode 100644 index 0000000..90a2da9 --- /dev/null +++ b/vcr/migration.py @@ -0,0 +1,104 @@ +""" +Migration script for old 'yaml' and 'json' cassettes + +.. warning:: Backup your cassettes files before migration. + +It merges and deletes the request obsolete keys (protocol, host, port, path) +into new 'uri' key. +Usage:: + + python -m vcr.migration PATH + +The PATH can be path to the directory with cassettes or cassette itself +""" + +from contextlib import closing +import json +import os +import re +import shutil +import sys +import tempfile + + +PARTS = [ + 'protocol', + 'host', + 'port', + 'path', +] + + +def build_uri(**parts): + return "{protocol}://{host}:{port}{path}".format(**parts) + + +def migrate_json(in_fp, out_fp): + data = json.load(in_fp) + for item in data: + req = item['request'] + uri = {k: req.pop(k) for k in PARTS} + req['uri'] = build_uri(**uri) + json.dump(data, out_fp, indent=4) + + +def migrate_yml(in_fp, out_fp): + migrated = False + uri = dict.fromkeys(PARTS, None) + for line in in_fp: + for part in uri: + match = re.match('\s+{}:\s(.*)'.format(part), line) + if match: + uri[part] = match.group(1) + break + else: + out_fp.write(line) + + if None not in uri.values(): # if all uri parts are collected + out_fp.write(" uri: {}\n".format(build_uri(**uri))) + uri = dict.fromkeys(PARTS, None) # reset dict + migrated = True + if not migrated: + raise RuntimeError("migration failed") + + +def migrate(file_path, migration_fn): + # because we assume that original files can be reverted + # we will try to copy the content. (os.rename not needed) + with closing(tempfile.TemporaryFile()) as out_fp: + with open(file_path, 'r') as in_fp: + migration_fn(in_fp, out_fp) + with open(file_path, 'w') as in_fp: + out_fp.seek(0) + shutil.copyfileobj(out_fp, in_fp) + + +def try_migrate(path): + try: # try to migrate as json + migrate(path, migrate_json) + except: # probably the file is not a json + try: # let's try to migrate as yaml + migrate(path, migrate_yml) + except: # oops probably the file is not a cassette + return False + return True + + +def main(): + if len(sys.argv) != 2: + raise SystemExit("Please provide path to cassettes directory or file. " + "Usage: python -m vcr.migration PATH") + + path = sys.argv[1] + if not os.path.isabs(path): + path = os.path.abspath(path) + for root, dirs, files in os.walk(path): + for file_name in files: + file_path = os.path.join(root, file_name) + migrated = try_migrate(file_path) + status = 'OK' if migrated else 'FAIL' + sys.stderr.write("[{}] {}\n".format(status, file_path)) + sys.stderr.write("Done.\n") + +if __name__ == '__main__': + main()