# HG changeset patch # User Sebastien Jodogne # Date 1434473369 -7200 # Node ID 08dadea8f40a302e4bdd49d0062d234e6c37b75f # Parent cc43b57242a431d006e77ea04eb1695f23a3339e start diff -r cc43b57242a4 -r 08dadea8f40a Database/DummyCT.dcm Binary file Database/DummyCT.dcm has changed diff -r cc43b57242a4 -r 08dadea8f40a ExternalCommandThread.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ExternalCommandThread.py Tue Jun 16 18:49:29 2015 +0200 @@ -0,0 +1,50 @@ +#!/usr/bin/python + +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import os +import signal +import subprocess +import threading + +class ExternalCommandThread: + @staticmethod + def ExternalCommandFunction(arg, stop_event, command, env): + external = subprocess.Popen(command, env = env) + + while (not stop_event.is_set()): + error = external.poll() + if error != None: + # http://stackoverflow.com/a/1489838/881731 + os._exit(-1) + stop_event.wait(0.1) + + print 'Stopping the external command' + external.terminate() + + def __init__(self, command, env = None): + self.thread_stop = threading.Event() + self.thread = threading.Thread(target = self.ExternalCommandFunction, + args = (10, self.thread_stop, command, env)) + self.daemon = True + self.thread.start() + + def stop(self): + self.thread_stop.set() + self.thread.join() diff -r cc43b57242a4 -r 08dadea8f40a Run.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Run.py Tue Jun 16 18:49:29 2015 +0200 @@ -0,0 +1,180 @@ +#!/usr/bin/python + +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +# sudo docker run --rm -t -i -v `pwd`:/tmp/tests:ro -p 5000:8042 -p 5001:4242 --entrypoint python jodogne/orthanc-tests /tmp/tests/Run.py --force + + + +import re +import sys +import argparse +import subprocess +import unittest + +from ExternalCommandThread import * +from Toolbox import * + + +## +## Parse the command-line arguments +## + +parser = argparse.ArgumentParser(description = 'Run the integration tests on some instance of Orthanc.') +parser.add_argument('--server', + default = GetDockerHostAddress(), + help = 'Address of the Orthanc server to test') +parser.add_argument('--aet', + default = 'ORTHANC', + help = 'AET of the Orthanc instance to test') +parser.add_argument('--dicom', + type = int, + default = 4242, + help = 'DICOM port of the Orthanc instance to test') +parser.add_argument('--rest', + type = int, + default = 8042, + help = 'Port to the REST API') +parser.add_argument('--username', + default = None, + help = 'Username to the REST API') +parser.add_argument('--password', + default = None, + help = 'Password to the REST API') +parser.add_argument("--force", help = "Do not warn the user", + action = "store_true") + +args = parser.parse_args() + +if not args.force: + print(""" +WARNING: This test will remove all the content of your +Orthanc instance running on %s! + +Are you sure ["yes" to go on]?""" % args.server) + + if sys.stdin.readline().strip() != 'yes': + print('Aborting...') + exit(0) + + + +## +## Generate the configuration file for the anciliary instance of +## Orthanc +## + +CONFIG = '/tmp/Configuration.json' +subprocess.check_call([ 'Orthanc', '--config=%s' % CONFIG ]) + +with open(CONFIG, 'r') as f: + config = f.read() + +config = re.sub(r'("StorageDirectory"\s*:)\s*".*?"', r'\1 "/tmp/OrthancStorage"', config) +config = re.sub(r'("IndexDirectory"\s*:)\s*".*?"', r'\1 "/tmp/OrthancStorage"', config) +config = re.sub(r'("DicomAet"\s*:)\s*".*?"', r'\1 "ORTHANCTEST"', config) +config = re.sub(r'("RemoteAccessAllowed"\s*:)\s*false', r'\1 true', config) +config = re.sub(r'("AuthenticationEnabled"\s*:)\s*false', r'\1 true', config) +config = re.sub(r'("RegisteredUsers"\s*:)\s*{', r'\1 { "alice" : [ "orthanctest" ]', config) +config = re.sub(r'("DicomModalities"\s*:)\s*{', r'\1 { "orthanc" : [ "%s", "%s", "%s" ]' % + (args.aet, args.server, args.dicom), config) + +localOrthanc = ExternalCommandThread([ + 'Orthanc', CONFIG, #'--verbose' + ]) + + +LOCAL = DefineOrthanc(aet = 'ORTHANCTEST') +REMOTE = DefineOrthanc(url = 'http://%s:%d/' % (args.server, args.rest), + username = args.username, + password = args.password, + aet = args.aet, + dicomPort = args.dicom) + + + +class Orthanc(unittest.TestCase): + def setUp(self): + DropOrthanc(LOCAL) + DropOrthanc(REMOTE) + + def test_system(self): + self.assertTrue('Version' in DoGet(REMOTE, '/system')) + self.assertEqual('0', DoGet(REMOTE, '/statistics')['TotalDiskSize']) + self.assertEqual('0', DoGet(REMOTE, '/statistics')['TotalUncompressedSize']) + + def test_upload(self): + u = UploadInstance(REMOTE, 'DummyCT.dcm') + self.assertEqual('Success', u['Status']) + u = UploadInstance(REMOTE, 'DummyCT.dcm') + self.assertEqual('AlreadyStored', u['Status']) + self.assertEqual(1, len(DoGet(REMOTE, '/patients'))) + self.assertEqual(1, len(DoGet(REMOTE, '/studies'))) + self.assertEqual(1, len(DoGet(REMOTE, '/series'))) + self.assertEqual(1, len(DoGet(REMOTE, '/instances'))) + + i = DoGet(REMOTE, '/instances/%s/simplified-tags' % u['ID']) + self.assertEqual('20070101', i['StudyDate']) + + + def test_rest_grid(self): + i = UploadInstance(REMOTE, 'DummyCT.dcm')['ID'] + instance = DoGet(REMOTE, '/instances/%s' % i) + self.assertEqual(i, instance['ID']) + self.assertEqual('1.2.840.113619.2.176.2025.1499492.7040.1171286242.109', + instance['MainDicomTags']['SOPInstanceUID']) + + series = DoGet(REMOTE, '/series/%s' % instance['ParentSeries']) + self.assertEqual('1.2.840.113619.2.176.2025.1499492.7391.1171285944.394', + series['MainDicomTags']['SeriesInstanceUID']) + + study = DoGet(REMOTE, '/studies/%s' % series['ParentStudy']) + self.assertEqual('1.2.840.113619.2.176.2025.1499492.7391.1171285944.390', + study['MainDicomTags']['StudyInstanceUID']) + + patient = DoGet(REMOTE, '/patients/%s' % study['ParentPatient']) + self.assertEqual('ozp00SjY2xG', + patient['MainDicomTags']['PatientID']) + + dicom = DoGet(REMOTE, '/instances/%s/file' % instance['ID']) + self.assertEqual(2472, len(dicom)) + self.assertEqual('3e29b869978b6db4886355a2b1132124', ComputeMD5(dicom)) + self.assertEqual(1, len(DoGet(REMOTE, '/instances/%s/frames' % i))) + self.assertEqual('TWINOW', DoGet(REMOTE, '/instances/%s/simplified-tags' % i)['StationName']) + self.assertEqual('TWINOW', DoGet(REMOTE, '/instances/%s/tags' % i)['0008,1010']['Value']) + + +try: + print('Waiting for the internal Orthanc to start...') + while True: + try: + DoGet(LOCAL, '/instances') + break + except: + time.sleep(0.1) + + print('Starting the tests...') + unittest.main(argv = [ sys.argv[0] ]) #argv = args) + +finally: + # The tests have stopped or "Ctrl-C" has been hit + try: + localOrthanc.stop() + except: + pass diff -r cc43b57242a4 -r 08dadea8f40a Toolbox.py --- a/Toolbox.py Mon Jun 15 17:44:12 2015 +0200 +++ b/Toolbox.py Tue Jun 16 18:49:29 2015 +0200 @@ -1,18 +1,38 @@ #!/usr/bin/python -# sudo docker run --rm -v `pwd`/Toolbox.py:/tmp/Toolbox.py:ro --entrypoint python jodogne/orthanc-tests /tmp/Toolbox.py +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from PIL import Image +from urllib import urlencode import hashlib import httplib2 import json import os.path -from PIL import Image -import zipfile +import re +import subprocess import time -from urllib import urlencode +import zipfile +HERE = os.path.dirname(__file__) + # http://stackoverflow.com/a/1313868/881731 try: @@ -21,19 +41,33 @@ from StringIO import StringIO +def DefineOrthanc(url = 'http://localhost:8042', + username = None, + password = None, + aet = 'ORTHANC', + dicomPort = 4242): + m = re.match(r'(http|https)://([^:]+):([^@]+)@([^@]+)', url) + if m != None: + url = m.groups()[0] + '://' + m.groups()[3] + username = m.groups()[1] + password = m.groups()[2] -def CreateOrthanc(url = 'http://localhost:8042', - username = None, - password = None): if not url.endswith('/'): url += '/' - return [ url, username, password ] + return { + 'Url' : url, + 'Username' : username, + 'Password' : password, + 'DicomAet' : aet, + 'DicomPort' : dicomPort + } def _SetupCredentials(orthanc, http): - if orthanc[1] != None and orthanc[2] != None: - http.add_credentials(orthanc[1], orthanc[2]) + if (orthanc['Username'] != None and + orthanc['Password'] != None): + http.add_credentials(orthanc['Username'], orthanc['Password']) def DoGet(orthanc, uri, data = {}, body = None, headers = {}): @@ -44,7 +78,7 @@ http = httplib2.Http() _SetupCredentials(orthanc, http) - resp, content = http.request(orthanc[0] + uri + d, 'GET', body = body, + resp, content = http.request(orthanc['Url'] + uri + d, 'GET', body = body, headers = headers) if not (resp.status in [ 200 ]): raise Exception(resp.status) @@ -68,7 +102,7 @@ headers['expect'] = '' - resp, content = http.request(orthanc[0] + uri, method, + resp, content = http.request(orthanc['Url'] + uri, method, body = body, headers = headers) if not (resp.status in [ 200, 302 ]): @@ -83,7 +117,7 @@ http = httplib2.Http() _SetupCredentials(orthanc, http) - resp, content = http.request(orthanc[0] + uri, 'DELETE') + resp, content = http.request(orthanc['Url'] + uri, 'DELETE') if not (resp.status in [ 200 ]): raise Exception(resp.status) else: @@ -99,19 +133,21 @@ return _DoPutOrPost(orthanc, uri, 'POST', data, contentType, headers) def UploadInstance(orthanc, filename): - p = os.path.join(HERE, DICOM_DB, filename) + global HERE + p = os.path.join(HERE, 'Database', filename) f = open(p, 'rb') d = f.read() f.close() return DoPost(orthanc, '/instances', d, 'application/dicom') def UploadFolder(orthanc, path): - p = os.path.join(HERE, DICOM_DB, path) - for i in os.listdir(p): - try: - UploadInstance(orthanc, os.path.join(path, i)) - except: - pass + global HERE + p = os.path.join(HERE, 'Database', path) + for i in os.listdir(p): + try: + UploadInstance(orthanc, os.path.join(path, i)) + except: + pass def DropOrthanc(orthanc): # Reset the Lua callbacks @@ -137,15 +173,20 @@ s = DoGet(orthanc, uri) return zipfile.ZipFile(StringIO(s), "r") -def IsDefinedInLua(name): +def IsDefinedInLua(orthanc, name): s = DoPost(orthanc, '/tools/execute-script', 'print(type(%s))' % name, 'application/lua') return (s.strip() != 'nil') -def WaitEmpty(): +def WaitEmpty(orthanc): while True: - if len(orthanc, DoGet('/instances')) == 0: + if len(DoGet(orthanc, '/instances')) == 0: return time.sleep(0.1) - -print DoGet(CreateOrthanc('http://192.168.215.82:8042'), '/system') +def GetDockerHostAddress(): + route = subprocess.check_output([ '/sbin/ip', 'route' ]) + m = re.search(r'default via ([0-9.]+)', route) + if m == None: + return 'localhost' + else: + return m.groups()[0]