Page MenuHomeSoftware Heritage

No OneTemporary

diff --git a/README-uri-scheme.md b/README-uri-scheme.md
index fb29ae5c..0889e7b2 100644
--- a/README-uri-scheme.md
+++ b/README-uri-scheme.md
@@ -1,113 +1,146 @@
URI scheme
==========
Browsing namespace
------------------
### Global
To be anchored where browsing starts (e.g., at /api/1)
* /revision/<SHA1_GIT>: show commit information
+ $curl http://localhost:6543/api/1/revision/18d8be353ed3480476f032475e7c233eff7371d5
+ {
+ "revision": {
+ "author_email": "robot@softwareheritage.org",
+ "author_name": "Software Heritage",
+ "committer_date": "Mon, 17 Jan 2000 10:23:54 GMT",
+ "committer_date_offset": 0,
+ "committer_email": "robot@softwareheritage.org",
+ "committer_name": "Software Heritage",
+ "date": "Mon, 17 Jan 2000 10:23:54 GMT",
+ "date_offset": 0,
+ "directory": "7834ef7e7c357ce2af928115c6c6a42b7e2a44e6",
+ "id": "18d8be353ed3480476f032475e7c233eff7371d5",
+ "message": "synthetic revision message",
+ "metadata": {
+ "original_artifact": [
+ {
+ "archive_type": "tar",
+ "name": "webbase-5.7.0.tar.gz",
+ "sha1": "147f73f369733d088b7a6fa9c4e0273dcd3c7ccd",
+ "sha1_git": "6a15ea8b881069adedf11feceec35588f2cfe8f1",
+ "sha256": "401d0df797110bea805d358b85bcc1ced29549d3d73f309d36484e7edf7bb912"
+ }
+ ]
+ },
+ "parents": [
+ null
+ ],
+ "synthetic": true,
+ "type": "tar"
+ }
+ }
+
* /directory/<SHA1_GIT>: show directory information (including ls)
* /directory/<SHA1_GIT>/path/to/file-or-dir: ditto, but for dir pointed by path
- note: this is the same as /dir/<SHA1_GIT'>, where <SHA1_GIT'> is the
sha1_git ID of the dir pointed by path
* /content/[<HASH_ALGO>:]<HASH>: show content information
- content is specified by HASH, according to HASH_ALGO, where HASH_ALGO is
one of: sha1, sha1_git, sha256. This means that several different URLs (at
least one per HASH_ALGO) will point to the same content
- HASH_ALGO defaults to "sha1" (?)
* /release/<SHA1_GIT>: show release information
Sample:
$ curl -X GET http://localhost:6543/api/1/release/4a1b6d7dd0a923ed90156c4e2f5db030095d8e08
{
"release": {
"author": 1,
"comment": "synthetic release message",
"date": "Sat, 04 Mar 2000 07:50:35 GMT",
"date_offset": 0,
"id": "4a1b6d7dd0a923ed90156c4e2f5db030095d8e08",
"name": "4.0.6",
"revision": "5c7814ce9978d4e16f3858925b5cea611e500eec",
"synthetic": true
}
}%
* /person/<PERSON_ID>: show person information
* /origin/<ORIGIN_ID>: show origin information
Sample:
$ curl -X GET http://localhost:6543/api/1/origin/1
{
"origin": {
"id": 1,
"lister": null,
"project": null,
"type": "ftp",
"url": "rsync://ftp.gnu.org/old-gnu/solfege"
}
}%
* /project/<PROJECT_ID>: show project information
* /organization/<ORGANIZATION_ID>: show organization information
### Occurrence
Origin/Branch do not contain `|` so it is used as a terminator.
Origin is <TYPE+URL>.
Timestamp is one of: latest or an ISO8601 date (TODO: decide the time matching
policy).
* /directory/<TIMESTAMP>/<ORIGIN>|/<BRANCH>|/path/to/file-or-dir
- Same as /directory/<SHA1_GIT> but looking up sha1 git using origin and
branch at a given timestamp
* /revision/<TIMESTAMP>/<ORIGIN>|/<BRANCH>
- Same as /revision/<SHA1_GIT> but looking up sha1 git using origin and
branch at a given timestamp
* /revision/<TIMESTAMP>/<ORIGIN>|
- Show all branches of origin at a given timestamp
* /revision/<TIMESTAMP>/<ORIGIN>|/<BRANCH>|
- Show all revisions (~git log) of origin and branch at a given timestamp
### Upload and search
* /1/api/uploadnsearch/
Post a file's content to api.
Api computes the sha1 hash and checks in the storage if such sha1 exists.
Json answer:
{'sha1': hexadecimal sha1,
'found': true or false}
Sample:
$ curl -X POST -F filename=@/path/to/file http://localhost:6543/api/1/uploadnsearch/
{
"found": false,
"sha1": "e95097ad2d607b4c89c1ce7ca1fef2a1e4450558"
}%
Search namespace
----------------
diff --git a/swh/web/ui/controller.py b/swh/web/ui/controller.py
index 0b3abd22..18e60214 100644
--- a/swh/web/ui/controller.py
+++ b/swh/web/ui/controller.py
@@ -1,391 +1,400 @@
# Copyright (C) 2015 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
import json
from flask import render_template, jsonify, request, flash
from flask import make_response
from swh.core.hashutil import ALGORITHMS
from swh.web.ui.main import app
from swh.web.ui import service
from swh.web.ui.decorators import jsonp
hash_filter_keys = ALGORITHMS
@app.route('/')
def main():
"""Home page
"""
flash('This Web app is still work in progress, use at your own risk',
'warning')
# return redirect(url_for('about'))
return render_template('home.html')
@app.route('/about')
def about():
return render_template('about.html')
@app.route('/search')
def search():
"""Search for hashes in swh-storage.
"""
q = request.args.get('q', '')
env = {'q': q, 'message': '', 'found': None}
try:
if q:
env['found'] = service.lookup_hash(q)
except ValueError:
env['message'] = 'Error: invalid query string'
return render_template('search.html', **env)
@app.route('/uploadnsearch', methods=['GET', 'POST'])
def uploadnsearch():
"""Upload and search for hashes in swh-storage.
"""
env = {'filename': None, 'message': '', 'found': None}
if request.method == 'POST':
file = request.files['filename']
try:
filename, sha1, found = service.upload_and_search(file)
message = 'The file %s with hash %s has%sbeen found.' % (
filename,
sha1,
' ' if found else ' not ')
env.update({
'filename': filename,
'sha1': sha1,
'found': found,
'message': message
})
except ValueError:
env['message'] = 'Error: invalid query string'
return render_template('upload_and_search.html', **env)
@app.route('/browse/revision/<sha1_git>')
def revision(sha1_git):
"""Show commit information.
Args:
sha1_git: the revision's sha1
Returns:
Revision information
"""
return render_template('revision.html',
sha1_git=sha1_git)
@app.route('/browse/directory/<sha1_git>')
def directory(sha1_git):
"""Show directory information.
Args:
sha1_git: the directory's sha1
Returns:
Directory information
"""
return render_template('directory.html',
sha1_git=sha1_git)
@app.route('/browse/directory/<sha1_git>/<path:p>')
def directory_at_path(sha1_git, p):
"""Show directory information for the sha1_git at path.
Args:
sha1_git: the directory's sha1
path: file or directory pointed to
Returns:
Directory information at sha1_git + path
"""
return render_template('directory.html',
sha1_git=sha1_git,
path=p)
def _origin_seen(hash, data):
"""Given an origin, compute a message string with the right information.
Args:
origin: a dictionary with keys:
- origin: a dictionary with type and url keys
- occurrence: a dictionary with a validity range
Returns:
message as a string
"""
if data is None:
return 'Content with hash %s is unknown as of now.' % hash
origin_type = data['origin_type']
origin_url = data['origin_url']
revision = data['revision']
branch = data['branch']
path = data['path']
return """The content with hash %s has been seen on origin with type '%s'
at url '%s'. The revision was identified at '%s' on branch '%s'.
The file's path referenced was '%s'.""" % (hash,
origin_type,
origin_url,
revision,
branch,
path)
@app.route('/browse/content/<hash>:<sha>')
def content(hash, sha):
"""Show content information.
Args:
hash: hash according to HASH_ALGO, where HASH_ALGO is
one of: sha1, sha1_git, sha256. This means that several different URLs (at
least one per HASH_ALGO) will point to the same content
sha: the sha with 'hash' format
Returns:
The content's information at sha1_git
"""
if hash not in hash_filter_keys:
message = 'The checksum must be one of sha1, sha1_git, sha256'
else:
q = "%s:%s" % (hash, sha)
found = service.lookup_hash(q)
if not found:
message = "Hash %s was not found." % hash
else:
origin = service.lookup_hash_origin(q)
message = _origin_seen(hash, origin)
return render_template('content.html',
hash=hash,
sha=sha,
message=message)
@app.route('/browse/release/<sha1_git>')
def release(sha1_git):
"""Show release's information.
Args:
sha1_git: sha1_git for this particular release
Returns:
Release's information
"""
return 'Release information at %s' % sha1_git
@app.route('/browse/person/<int:id>')
def person(id):
"""Show Person's information at id.
Args:
id: person's unique identifier
Returns:
Person's information
"""
return 'Person information at %s' % id
@app.route('/browse/origin/<int:id>')
def origin(id):
"""Show origin's information at id.
Args:
id: origin's unique identifier
Returns:
Origin's information
"""
return 'Origin information at %s' % id
@app.route('/browse/project/<int:id>')
def project(id):
"""Show project's information at id.
Args:
id: project's unique identifier
Returns:
Project's information
"""
return 'Project information at %s' % id
@app.route('/browse/organization/<int:id>')
def organization(id):
"""Show organization's information at id.
Args:
id: organization's unique identifier
Returns:
Organization's information
"""
return 'Organization information at %s' % id
@app.route('/browse/directory/<string:timestamp>/'
'<string:origin_type>+<path:origin_url>|/'
'<path:branch>|/<path:path>')
def directory_at_origin(timestamp, origin_type, origin_url, branch, path):
"""Show directory information at timestamp, origin-type, origin-url, branch
and path.
Those parameters are separated by the `|` terminator.
Args:
timestamp: the timestamp to look for. can be latest or some iso8601
date format. (TODO: decide the time matching policy.)
origin_type: origin's type
origin_url: origin's url (can contain `/`)
branch: branch name which can contain `/`
path: path to directory or file
Returns:
Directory information at the given parameters.
"""
return 'Directory at (%s, %s, %s, %s, %s)' % (timestamp,
origin_type,
origin_url,
branch,
path)
@app.route('/browse/revision/<string:timestamp>/'
'<string:origin_type>+<path:origin_url>|/<path:branch>')
def revision_at_origin_and_branch(timestamp, origin_type, origin_url, branch):
"""Show revision information at timestamp, origin, and branch.
Those parameters are separated by the `|` terminator.
Args:
timestamp: the timestamp to look for. can be latest or some iso8601
date format. (TODO: decide the time matching policy.)
origin_type: origin's type
origin_url: origin's url (can contain `/`)
branch: branch name which can contain /
Returns:
Revision information at the given parameters.
"""
return 'Revision at (ts=%s, type=%s, url=%s, branch=%s)' % (timestamp,
origin_type,
origin_url,
branch)
@app.route('/browse/revision/<string:timestamp>/'
'<string:origin_type>+<path:origin_url>|')
def revision_at_origin(timestamp, origin_type, origin_url):
"""Show revision information at timestamp, origin, and branch.
Those parameters are separated by the `|` terminator.
Args:
timestamp: the timestamp to look for. can be latest or iso8601
date
format. (TODO: decide the time matching policy.)
origin_type: origin's type
origin_url: origin's url (can contain `/`)
Returns:
Revision information at the given parameters.
"""
return 'Revision at (timestamp=%s, type=%s, url=%s)' % (timestamp,
origin_type,
origin_url)
@app.route('/api/1/stat/counters')
@jsonp
def api_stats():
"""Return statistics as a JSON object"""
return jsonify(service.stat_counters())
@app.errorhandler(ValueError)
def value_error_as_bad_request(error):
"""Compute a bad request and add body as payload.
"""
response = make_response(
'Bad request',
400)
response.headers['Content-Type'] = 'application/json'
response.data = json.dumps({"error": str(error)})
return response
@app.route('/api/1/search/<string:q>/')
@jsonp
def api_search(q):
"""Return search results as a JSON object"""
return jsonify({'found': service.lookup_hash(q)})
@app.route('/api/1/origin/<string:origin_id>')
@jsonp
def api_origin(origin_id):
"""Return information about origin"""
return jsonify({'origin': service.lookup_origin(origin_id)})
@app.route('/api/1/release/<string:sha1_git>')
@jsonp
def api_release(sha1_git):
"""Return information about release with id sha1_git."""
return jsonify({'release': service.lookup_release(sha1_git)})
+@app.route('/api/1/revision/<string:sha1_git>')
+@jsonp
+def api_revision(sha1_git):
+ """Return information about revision with id sha1_git.
+
+ """
+ return jsonify({'revision': service.lookup_revision(sha1_git)})
+
+
@app.route('/api/1/browse/<string:q>/')
@jsonp
def api_browse(q):
"""Return search results as a JSON object"""
return jsonify({'origin': service.lookup_hash_origin(q)})
@app.route('/api/1/uploadnsearch/', methods=['POST'])
@jsonp
def api_uploadnsearch():
"""Upload the file's content in the post body request.
Compute the hash and determine if it exists in the storage.
"""
file = request.files['filename']
filename, sha1, found = service.upload_and_search(file)
return jsonify({'sha1': sha1,
'filename': filename,
'found': found})
diff --git a/swh/web/ui/converters.py b/swh/web/ui/converters.py
index 4cbd78e2..cb391a2b 100644
--- a/swh/web/ui/converters.py
+++ b/swh/web/ui/converters.py
@@ -1,56 +1,99 @@
# Copyright (C) 2015 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
from swh.core import hashutil
def from_origin(origin):
"""Convert from an swh origin to an origin dictionary.
"""
new_origin = {}
for key, value in origin.items():
if key == 'revision':
new_origin[key] = hashutil.hash_to_hex(value)
elif key == 'path':
new_origin[key] = value.decode('utf-8')
else:
new_origin[key] = value
return new_origin
def from_release(release):
"""Convert from an swh release to a json serializable release dictionary.
Args:
release: Dict with the following keys
- id: identifier of the revision (sha1 in bytes)
- revision: identifier of the revision the release points to (sha1 in
bytes)
- comment: release's comment message (bytes)
- name: release's name (string)
- author: release's author identifier (swh's id)
- synthetic: the synthetic property (boolean)
Returns:
Release dictionary with the following keys:
- id: hexadecimal sha1 (string)
- revision: hexadecimal sha1 (string)
- comment: release's comment message (string)
- name: release's name (string)
- author: release's author identifier (swh's id)
- synthetic: the synthetic property (boolean)
"""
new_release = {}
for key, value in release.items():
if key == 'id' or key == 'revision':
new_release[key] = hashutil.hash_to_hex(value) if value else None
elif key == 'comment':
new_release[key] = value.decode('utf-8')
else:
new_release[key] = value
return new_release
+
+
+def from_revision(revision):
+ """Convert from an SWH revision to a json serializable revision dictionary.
+
+ Args:
+ revision: Dict with the following keys
+ - id: identifier of the revision (sha1 in bytes)
+ - directory: identifier of the directory the revision points to (sha1
+ in bytes)
+ - author_name, author_email: author's revision name and email
+ - committer_name, committer_email: committer's revision name and email
+ - message: revision's message
+ - date, date_offset: revision's author date
+ - committer_date, committer_date_offset: revision's commit date
+ - parents: list of parents for such revision
+ - synthetic: revision's property nature
+ - type: revision's type (git, tar or dsc at the moment)
+ - metadata: if the revision is synthetic, this can reference dynamic
+ properties.
+
+ Returns:
+ Revision dictionary with the same keys as inputs, only:
+ - sha1s are in hexadecimal strings (id, directory)
+ - bytes are decoded in string (author_name, committer_name,
+ author_email, committer_email, message)
+ - remaining keys are left as is
+
+ """
+ new_revision = {}
+ for key, value in revision.items():
+ if key in ['id', 'directory']:
+ new_revision[key] = hashutil.hash_to_hex(value) if value else None
+ elif key in ['author_name',
+ 'committer_name',
+ 'author_email',
+ 'committer_email',
+ 'message']:
+ new_revision[key] = value.decode('utf-8')
+ else:
+ new_revision[key] = value
+
+ return new_revision
diff --git a/swh/web/ui/service.py b/swh/web/ui/service.py
index a0b7031f..ae0602b1 100644
--- a/swh/web/ui/service.py
+++ b/swh/web/ui/service.py
@@ -1,115 +1,139 @@
# Copyright (C) 2015 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
from swh.core import hashutil
from swh.web.ui import converters, main, query, upload
def hash_and_search(filepath):
"""Hash the filepath's content as sha1, then search in storage if
it exists.
Args:
Filepath of the file to hash and search.
Returns:
Tuple (hex sha1, found as True or false).
The found boolean, according to whether the sha1 of the file
is present or not.
"""
hash = hashutil.hashfile(filepath)
return (hashutil.hash_to_hex(hash['sha1']),
main.storage().content_exist(hash))
def upload_and_search(file):
"""Upload a file and compute its hash.
"""
tmpdir, filename, filepath = upload.save_in_upload_folder(file)
try:
sha1, found = None, None
if filepath:
sha1, found = hash_and_search(filepath)
return filename, sha1, found
finally:
# clean up
if tmpdir:
upload.cleanup(tmpdir)
def lookup_hash(q):
"""Checks if the storage contains a given content checksum
Args: query string
Returns:
True or False, according to whether the checksum is present or not
"""
(algo, hash) = query.parse_hash(q)
return main.storage().content_exist({algo: hash})
def lookup_hash_origin(q):
"""Return information about the checksum contained in the query q.
Args: query string
Returns:
True or False, according to whether the checksum is present or not
"""
algo, h = query.parse_hash(q)
origin = main.storage().content_find_occurrence({algo: h})
return converters.from_origin(origin)
def lookup_origin(origin_id):
"""Return information about the origin with id origin_id.
Args:
origin_id as string
Returns:
origin information as dict.
"""
return main.storage().origin_get({'id': origin_id})
def lookup_release(release_sha1_git):
"""Return information about the release with sha1 release_sha1_git.
Args:
release_sha1_git: The release's sha1 as hexadecimal
Returns:
Release information as dict.
Raises:
ValueError if the identifier provided is not of sha1 nature.
"""
algo, hBinSha1 = query.parse_hash(release_sha1_git)
if algo != 'sha1': # HACK: sha1_git really but they are both sha1...
raise ValueError('Only sha1_git is supported.')
res = main.storage().release_get([hBinSha1])
if res and len(res) >= 1:
return converters.from_release(res[0])
return None
+def lookup_revision(rev_sha1_git, rev_type='git'):
+ """Return information about the revision with sha1 revision_sha1_git.
+
+ Args:
+ revision_sha1_git: The revision's sha1 as hexadecimal
+
+ Returns:
+ Revision information as dict.
+
+ Raises:
+ ValueError if the identifier provided is not of sha1 nature.
+
+ """
+ algo, hBinSha1 = query.parse_hash(rev_sha1_git)
+ if algo != 'sha1': # HACK: sha1_git really but they are both sha1...
+ raise ValueError('Only sha1_git is supported.')
+
+ res = main.storage().revision_get([hBinSha1])
+
+ if res and len(res) >= 1:
+ return converters.from_revision(res[0])
+ return None
+
+
def stat_counters():
"""Return the stat counters for Software Heritage
Returns:
A dict mapping textual labels to integer values.
"""
return main.storage().stat_counters()
diff --git a/swh/web/ui/tests/test_controller.py b/swh/web/ui/tests/test_controller.py
index 38aef1ef..8ea905de 100644
--- a/swh/web/ui/tests/test_controller.py
+++ b/swh/web/ui/tests/test_controller.py
@@ -1,220 +1,260 @@
# Copyright (C) 2015 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
import unittest
import json
from nose.tools import istest
from unittest.mock import patch
from swh.web.ui.tests import test_app
class ApiTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.app, _, _ = test_app.init_app()
@istest
def info(self):
# when
rv = self.app.get('/about')
self.assertEquals(rv.status_code, 200)
self.assertIn(b'About', rv.data)
# @istest
def search_1(self):
# when
rv = self.app.get('/search')
self.assertEquals(rv.status_code, 200) # check this api
self.assertRegexpMatches(rv.data, b'name=q value=>')
# @istest
def search_2(self):
# when
rv = self.app.get('/search?q=one-hash-to-look-for:another-one')
self.assertEquals(rv.status_code, 200) # check this api
self.assertRegexpMatches(
rv.data,
b'name=q value=one-hash-to-look-for:another-one')
@patch('swh.web.ui.controller.service')
@istest
def api_browse(self, mock_service):
# given
mock_service.lookup_hash_origin.return_value = {
'origin': 'some-origin'
}
# when
rv = self.app.get('/api/1/browse/sha1:foo/')
self.assertEquals(rv.status_code, 200)
self.assertEquals(rv.mimetype, 'application/json')
response_data = json.loads(rv.data.decode('utf-8'))
self.assertEquals(response_data, {'origin': {'origin': 'some-origin'}})
mock_service.lookup_hash_origin.assert_called_once_with('sha1:foo')
@patch('swh.web.ui.controller.service')
@istest
def api_search(self, mock_service):
# given
mock_service.lookup_hash.return_value = False
# when
rv = self.app.get('/api/1/search/sha1:blah/')
self.assertEquals(rv.status_code, 200)
self.assertEquals(rv.mimetype, 'application/json')
response_data = json.loads(rv.data.decode('utf-8'))
self.assertEquals(response_data, {'found': False})
mock_service.lookup_hash.assert_called_once_with('sha1:blah')
@patch('swh.web.ui.controller.service')
@istest
def api_1_stat_counters_raise_error(self, mock_service):
# given
mock_service.stat_counters.side_effect = ValueError(
'voluntary error to check the bad request middleware.')
# when
rv = self.app.get('/api/1/stat/counters')
# then
self.assertEquals(rv.status_code, 400)
self.assertEquals(rv.mimetype, 'application/json')
response_data = json.loads(rv.data.decode('utf-8'))
self.assertEquals(response_data, {
'error': 'voluntary error to check the bad request middleware.'})
@patch('swh.web.ui.controller.service')
@istest
def api_1_stat_counters(self, mock_service):
# given
mock_service.stat_counters.return_value = {
"content": 1770830,
"directory": 211683,
"directory_entry_dir": 209167,
"directory_entry_file": 1807094,
"directory_entry_rev": 0,
"entity": 0,
"entity_history": 0,
"occurrence": 0,
"occurrence_history": 19600,
"origin": 1096,
"person": 0,
"release": 8584,
"revision": 7792,
"revision_history": 0,
"skipped_content": 0
}
# when
rv = self.app.get('/api/1/stat/counters')
response_data = json.loads(rv.data.decode('utf-8'))
self.assertEquals(rv.status_code, 200)
self.assertEquals(rv.mimetype, 'application/json')
self.assertEquals(response_data, {
"content": 1770830,
"directory": 211683,
"directory_entry_dir": 209167,
"directory_entry_file": 1807094,
"directory_entry_rev": 0,
"entity": 0,
"entity_history": 0,
"occurrence": 0,
"occurrence_history": 19600,
"origin": 1096,
"person": 0,
"release": 8584,
"revision": 7792,
"revision_history": 0,
"skipped_content": 0
})
mock_service.stat_counters.assert_called_once_with()
@patch('swh.web.ui.controller.service')
@patch('swh.web.ui.controller.request')
@istest
def api_uploadnsearch(self, mock_request, mock_service):
# given
mock_request.files = {'filename': 'simple-filename'}
mock_service.upload_and_search.return_value = (
'simple-filename', 'some-hex-sha1', False)
# when
rv = self.app.post('/api/1/uploadnsearch/')
self.assertEquals(rv.status_code, 200)
self.assertEquals(rv.mimetype, 'application/json')
response_data = json.loads(rv.data.decode('utf-8'))
self.assertEquals(response_data, {'filename': 'simple-filename',
'sha1': 'some-hex-sha1',
'found': False})
mock_service.upload_and_search.assert_called_once_with(
'simple-filename')
@patch('swh.web.ui.controller.service')
@istest
def api_origin(self, mock_service):
# given
mock_service.lookup_origin.return_value = {
'id': 'origin-0',
'lister': 'uuid-lister-0',
'project': 'uuid-project-0',
'url': 'ftp://some/url/to/origin/0',
'type': 'ftp'}
# when
rv = self.app.get('/api/1/origin/origin-0')
# then
self.assertEquals(rv.status_code, 200)
self.assertEquals(rv.mimetype, 'application/json')
response_data = json.loads(rv.data.decode('utf-8'))
self.assertEquals(response_data, {
'origin': {
'id': 'origin-0',
'lister': 'uuid-lister-0',
'project': 'uuid-project-0',
'url': 'ftp://some/url/to/origin/0',
'type': 'ftp'
}
})
@patch('swh.web.ui.controller.service')
@istest
def api_release(self, mock_service):
# given
mock_service.lookup_release.return_value = {
'id': 'release-0',
'revision': 'revision-sha1',
'author': 'author-id',
}
# when
rv = self.app.get('/api/1/release/release-0')
# then
self.assertEquals(rv.status_code, 200)
self.assertEquals(rv.mimetype, 'application/json')
response_data = json.loads(rv.data.decode('utf-8'))
self.assertEquals(response_data, {
'release': {
'id': 'release-0',
'revision': 'revision-sha1',
'author': 'author-id',
}
})
+
+ @patch('swh.web.ui.controller.service')
+ @istest
+ def api_revision(self, mock_service):
+ # given
+ mock_revision = {
+ 'id': '18d8be353ed3480476f032475e7c233eff7371d5',
+ 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6',
+ 'author_name': 'Software Heritage',
+ 'author_email': 'robot@softwareheritage.org',
+ 'committer_name': 'Software Heritage',
+ 'committer_email': 'robot@softwareheritage.org',
+ 'message': 'synthetic revision message',
+ 'date_offset': 0,
+ 'committer_date_offset': 0,
+ 'parents': [],
+ 'type': 'tar',
+ 'synthetic': True,
+ 'metadata': {
+ 'original_artifact': [{
+ 'archive_type': 'tar',
+ 'name': 'webbase-5.7.0.tar.gz',
+ 'sha1': '147f73f369733d088b7a6fa9c4e0273dcd3c7ccd',
+ 'sha1_git': '6a15ea8b881069adedf11feceec35588f2cfe8f1',
+ 'sha256': '401d0df797110bea805d358b85bcc1ced29549d3d73f'
+ '309d36484e7edf7bb912'
+ }]
+ },
+ }
+ mock_service.lookup_revision.return_value = mock_revision
+
+ # when
+ rv = self.app.get('/api/1/revision/revision-0')
+
+ # then
+ self.assertEquals(rv.status_code, 200)
+ self.assertEquals(rv.mimetype, 'application/json')
+
+ response_data = json.loads(rv.data.decode('utf-8'))
+ self.assertEquals(response_data, {"revision": mock_revision})
diff --git a/swh/web/ui/tests/test_converters.py b/swh/web/ui/tests/test_converters.py
index f969bd30..ca7582eb 100644
--- a/swh/web/ui/tests/test_converters.py
+++ b/swh/web/ui/tests/test_converters.py
@@ -1,103 +1,170 @@
# Copyright (C) 2015 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
import datetime
import unittest
from nose.tools import istest
from swh.core import hashutil
from swh.web.ui import converters
class ConvertersTestCase(unittest.TestCase):
@istest
def from_origin(self):
# given
origin_input = {
'origin_type': 'ftp',
'origin_url': 'rsync://ftp.gnu.org/gnu/octave',
'branch': 'octave-3.4.0.tar.gz',
'revision': b'\xb0L\xaf\x10\xe9SQ`\xd9\x0e\x87KE\xaaBm\xe7b\xf1\x9f', # noqa
'path': b'octave-3.4.0/doc/interpreter/octave.html/doc_002dS_005fISREG.html' # noqa
}
expected_origin = {
'origin_type': 'ftp',
'origin_url': 'rsync://ftp.gnu.org/gnu/octave',
'branch': 'octave-3.4.0.tar.gz',
'revision': 'b04caf10e9535160d90e874b45aa426de762f19f',
'path': 'octave-3.4.0/doc/interpreter/octave.html/doc_002dS_005fISREG.html' # noqa
}
# when
actual_origin = converters.from_origin(origin_input)
# then
self.assertEqual(actual_origin, expected_origin)
@istest
def from_release(self):
release_input = {
'id': hashutil.hex_to_hash(
'aad23fa492a0c5fed0708a6703be875448c86884'),
'revision': hashutil.hex_to_hash(
'5e46d564378afc44b31bb89f99d5675195fbdf67'),
'date': datetime.datetime(2015, 1, 1, 22, 0, 0,
tzinfo=datetime.timezone.utc),
'date_offset': None,
'name': 'v0.0.1',
'comment': b'some comment on release',
'synthetic': True,
}
expected_release = {
'id': 'aad23fa492a0c5fed0708a6703be875448c86884',
'revision': '5e46d564378afc44b31bb89f99d5675195fbdf67',
'date': datetime.datetime(2015, 1, 1, 22, 0, 0,
tzinfo=datetime.timezone.utc),
'date_offset': None,
'name': 'v0.0.1',
'comment': 'some comment on release',
'synthetic': True,
}
# when
actual_release = converters.from_release(release_input)
# then
self.assertEqual(actual_release, expected_release)
@istest
def from_release_no_revision(self):
release_input = {
'id': hashutil.hex_to_hash(
'b2171ee2bdf119cd99a7ec7eff32fa8013ef9a4e'),
'revision': None,
'date': datetime.datetime(2016, 3, 2, 10, 0, 0,
tzinfo=datetime.timezone.utc),
'date_offset': 1,
'name': 'v0.1.1',
'comment': b'comment on release',
'synthetic': False,
}
expected_release = {
'id': 'b2171ee2bdf119cd99a7ec7eff32fa8013ef9a4e',
'revision': None,
'date': datetime.datetime(2016, 3, 2, 10, 0, 0,
tzinfo=datetime.timezone.utc),
'date_offset': 1,
'name': 'v0.1.1',
'comment': 'comment on release',
'synthetic': False,
}
# when
actual_release = converters.from_release(release_input)
# then
self.assertEqual(actual_release, expected_release)
+
+ @istest
+ def from_revision(self):
+ revision_input = {
+ 'id': hashutil.hex_to_hash(
+ '18d8be353ed3480476f032475e7c233eff7371d5'),
+ 'directory': hashutil.hex_to_hash(
+ '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6'),
+ 'author_name': b'Software Heritage',
+ 'author_email': b'robot@softwareheritage.org',
+ 'committer_name': b'Software Heritage',
+ 'committer_email': b'robot@softwareheritage.org',
+ 'message': b'synthetic revision message',
+ 'date': datetime.datetime(2000, 1, 17, 11, 23, 54, tzinfo=None),
+ 'date_offset': 0,
+ 'committer_date': datetime.datetime(2000, 1, 17, 11, 23, 54,
+ tzinfo=None),
+ 'committer_date_offset': 0,
+ 'synthetic': True,
+ 'type': 'tar',
+ 'parents': [],
+ 'metadata': {
+ 'original_artifact': [{
+ 'archive_type': 'tar',
+ 'name': 'webbase-5.7.0.tar.gz',
+ 'sha1': '147f73f369733d088b7a6fa9c4e0273dcd3c7ccd',
+ 'sha1_git': '6a15ea8b881069adedf11feceec35588f2cfe8f1',
+ 'sha256': '401d0df797110bea805d358b85bcc1ced29549d3d73f'
+ '309d36484e7edf7bb912',
+
+ }]
+ },
+ }
+
+ expected_revision = {
+ 'id': '18d8be353ed3480476f032475e7c233eff7371d5',
+ 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6',
+ 'author_name': 'Software Heritage',
+ 'author_email': 'robot@softwareheritage.org',
+ 'committer_name': 'Software Heritage',
+ 'committer_email': 'robot@softwareheritage.org',
+ 'message': 'synthetic revision message',
+ 'date': datetime.datetime(2000, 1, 17, 11, 23, 54, tzinfo=None),
+ 'date_offset': 0,
+ 'committer_date': datetime.datetime(2000, 1, 17, 11, 23, 54,
+ tzinfo=None),
+ 'committer_date_offset': 0,
+ 'parents': [],
+ 'type': 'tar',
+ 'synthetic': True,
+ 'metadata': {
+ 'original_artifact': [{
+ 'archive_type': 'tar',
+ 'name': 'webbase-5.7.0.tar.gz',
+ 'sha1': '147f73f369733d088b7a6fa9c4e0273dcd3c7ccd',
+ 'sha1_git': '6a15ea8b881069adedf11feceec35588f2cfe8f1',
+ 'sha256': '401d0df797110bea805d358b85bcc1ced29549d3d73f'
+ '309d36484e7edf7bb912'
+ }]
+ },
+ }
+
+ # when
+ actual_revision = converters.from_revision(revision_input)
+
+ # then
+ self.assertEqual(actual_revision, expected_revision)
diff --git a/swh/web/ui/tests/test_service.py b/swh/web/ui/tests/test_service.py
index a956fe8c..7935a680 100644
--- a/swh/web/ui/tests/test_service.py
+++ b/swh/web/ui/tests/test_service.py
@@ -1,237 +1,285 @@
# Copyright (C) 2015 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
+import datetime
import unittest
from nose.tools import istest
from unittest.mock import MagicMock, patch
from swh.core.hashutil import hex_to_hash
from swh.web.ui import service
from swh.web.ui.tests import test_app
class ServiceTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
_, _, cls.storage = test_app.init_app()
@istest
def lookup_hash_does_not_exist(self):
# given
self.storage.content_exist = MagicMock(return_value=False)
# when
actual_lookup = service.lookup_hash(
'sha1:123caf10e9535160d90e874b45aa426de762f19f')
# then
self.assertFalse(actual_lookup)
# check the function has been called with parameters
self.storage.content_exist.assert_called_with({
'sha1':
hex_to_hash('123caf10e9535160d90e874b45aa426de762f19f')})
@istest
def lookup_hash_exist(self):
# given
self.storage.content_exist = MagicMock(return_value=True)
# when
actual_lookup = service.lookup_hash(
'sha1:456caf10e9535160d90e874b45aa426de762f19f')
# then
self.assertTrue(actual_lookup)
self.storage.content_exist.assert_called_with({
'sha1':
hex_to_hash('456caf10e9535160d90e874b45aa426de762f19f')})
@istest
def lookup_hash_origin(self):
# given
self.storage.content_find_occurrence = MagicMock(return_value={
'origin_type': 'sftp',
'origin_url': 'sftp://ftp.gnu.org/gnu/octave',
'branch': 'octavio-3.4.0.tar.gz',
'revision': b'\xb0L\xaf\x10\xe9SQ`\xd9\x0e\x87KE\xaaBm\xe7b\xf1\x9f', # noqa
'path': b'octavio-3.4.0/doc/interpreter/octave.html/doc_002dS_005fISREG.html' # noqa
})
expected_origin = {
'origin_type': 'sftp',
'origin_url': 'sftp://ftp.gnu.org/gnu/octave',
'branch': 'octavio-3.4.0.tar.gz',
'revision': 'b04caf10e9535160d90e874b45aa426de762f19f',
'path': 'octavio-3.4.0/doc/interpreter/octave.html/doc'
'_002dS_005fISREG.html'
}
# when
actual_origin = service.lookup_hash_origin(
'sha1_git:456caf10e9535160d90e874b45aa426de762f19f')
# then
self.assertEqual(actual_origin, expected_origin)
self.storage.content_find_occurrence.assert_called_with(
{'sha1_git':
hex_to_hash('456caf10e9535160d90e874b45aa426de762f19f')})
@istest
def stat_counters(self):
# given
input_stats = {
"content": 1770830,
"directory": 211683,
"directory_entry_dir": 209167,
"directory_entry_file": 1807094,
"directory_entry_rev": 0,
"entity": 0,
"entity_history": 0,
"occurrence": 0,
"occurrence_history": 19600,
"origin": 1096,
"person": 0,
"release": 8584,
"revision": 7792,
"revision_history": 0,
"skipped_content": 0
}
self.storage.stat_counters = MagicMock(return_value=input_stats)
# when
actual_stats = service.stat_counters()
# then
expected_stats = input_stats
self.assertEqual(actual_stats, expected_stats)
self.storage.stat_counters.assert_called_with()
@istest
def hash_and_search(self):
# given
self.storage.content_exist = MagicMock(return_value=False)
bhash = hex_to_hash('456caf10e9535160d90e874b45aa426de762f19f')
# when
with patch(
'swh.core.hashutil.hashfile',
return_value={'sha1': bhash}):
actual_hash, actual_search = service.hash_and_search('/some/path')
# then
self.assertEqual(actual_hash,
'456caf10e9535160d90e874b45aa426de762f19f')
self.assertFalse(actual_search)
self.storage.content_exist.assert_called_with({'sha1': bhash})
@patch('swh.web.ui.service.upload')
@istest
def test_upload_and_search_upload_OK_basic_case(self, mock_upload):
# given (cf. decorators patch)
mock_upload.save_in_upload_folder.return_value = (
'/tmp/blah', 'some-filename', None)
mock_upload.cleanup.return_value = None
file = MagicMock()
file.filename = 'some-filename'
# when
actual_file, actual_hash, actual_search = service.upload_and_search(
file)
# then
self.assertEqual(actual_file, 'some-filename')
self.assertIsNone(actual_hash)
self.assertIsNone(actual_search)
mock_upload.save_in_upload_folder.assert_called_with(file)
mock_upload.cleanup.assert_called_with('/tmp/blah')
@istest
def lookup_origin(self):
# given
self.storage.origin_get = MagicMock(return_value={
'id': 'origin-id',
'lister': 'uuid-lister',
'project': 'uuid-project',
'url': 'ftp://some/url/to/origin',
'type': 'ftp'})
# when
actual_origin = service.lookup_origin('origin-id')
# then
self.assertEqual(actual_origin, {'id': 'origin-id',
'lister': 'uuid-lister',
'project': 'uuid-project',
'url': 'ftp://some/url/to/origin',
'type': 'ftp'})
self.storage.origin_get.assert_called_with({'id': 'origin-id'})
@istest
def lookup_release(self):
- import datetime
# given
self.storage.release_get = MagicMock(return_value=[{
'id': hex_to_hash('65a55bbdf3629f916219feb3dcc7393ded1bc8db'),
'revision': None,
'date': datetime.datetime(2015, 1, 1, 22, 0, 0,
tzinfo=datetime.timezone.utc),
'date_offset': None,
'name': 'v0.0.1',
'comment': b'synthetic release',
'synthetic': True,
}])
# when
actual_release = service.lookup_release(
'65a55bbdf3629f916219feb3dcc7393ded1bc8db')
# then
self.assertEqual(actual_release, {
'id': '65a55bbdf3629f916219feb3dcc7393ded1bc8db',
'revision': None,
'date': datetime.datetime(2015, 1, 1, 22, 0, 0,
tzinfo=datetime.timezone.utc),
'date_offset': None,
'name': 'v0.0.1',
'comment': 'synthetic release',
'synthetic': True,
})
self.storage.release_get.assert_called_with(
[hex_to_hash('65a55bbdf3629f916219feb3dcc7393ded1bc8db')])
@istest
def lookup_release_ko_id_checksum_not_ok_because_not_a_sha1(self):
# given
self.storage.release_get = MagicMock()
with self.assertRaises(ValueError) as cm:
# when
service.lookup_release('not-a-sha1')
self.assertIn('invalid checksum', cm.exception.args[0])
self.storage.release_get.called = False
@istest
def lookup_release_ko_id_checksum_ok_but_not_a_sha1(self):
# given
self.storage.release_get = MagicMock()
# when
with self.assertRaises(ValueError) as cm:
service.lookup_release(
'13c1d34d138ec13b5ebad226dc2528dc7506c956e4646f62d4daf5'
'1aea892abe')
self.assertIn('sha1_git supported', cm.exception.args[0])
self.storage.release_get.called = False
+
+ @istest
+ def lookup_revision(self):
+ # given
+ self.storage.revision_get = MagicMock(return_value=[{
+ 'id': hex_to_hash('18d8be353ed3480476f032475e7c233eff7371d5'),
+ 'directory': hex_to_hash(
+ '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6'),
+ 'author_name': b'bill & boule',
+ 'author_email': b'bill@boule.org',
+ 'committer_name': b'boule & bill',
+ 'committer_email': b'boule@bill.org',
+ 'message': b'elegant fix for bug 31415957',
+ 'date': datetime.datetime(2000, 1, 17, 11, 23, 54),
+ 'date_offset': 0,
+ 'committer_date': datetime.datetime(2000, 1, 17, 11, 23, 54),
+ 'committer_date_offset': 0,
+ 'synthetic': False,
+ 'type': 'git',
+ 'parents': [],
+ 'metadata': [],
+ }])
+
+ # when
+ actual_revision = service.lookup_revision(
+ '18d8be353ed3480476f032475e7c233eff7371d5')
+
+ # then
+ self.assertEqual(actual_revision, {
+ 'id': '18d8be353ed3480476f032475e7c233eff7371d5',
+ 'directory': '7834ef7e7c357ce2af928115c6c6a42b7e2a44e6',
+ 'author_name': 'bill & boule',
+ 'author_email': 'bill@boule.org',
+ 'committer_name': 'boule & bill',
+ 'committer_email': 'boule@bill.org',
+ 'message': 'elegant fix for bug 31415957',
+ 'date': datetime.datetime(2000, 1, 17, 11, 23, 54),
+ 'date_offset': 0,
+ 'committer_date': datetime.datetime(2000, 1, 17, 11, 23, 54),
+ 'committer_date_offset': 0,
+ 'synthetic': False,
+ 'type': 'git',
+ 'parents': [],
+ 'metadata': [],
+ })
+
+ self.storage.revision_get.assert_called_with(
+ [hex_to_hash('18d8be353ed3480476f032475e7c233eff7371d5')])

File Metadata

Mime Type
text/x-diff
Expires
Fri, Jul 4, 3:29 PM (1 w, 1 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3302768

Event Timeline