#!/usr/bin/env python # Copyright 2014 Hewlett-Packard Development Company, L.P. # Copyright 2014 Rackspace Australia # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import asyncio import threading import os import json import urllib import time import socket import zuul.web from tests.base import ZuulTestCase, FIXTURE_DIR class FakeConfig(object): def __init__(self, config): self.config = config or {} def has_option(self, section, option): return option in self.config.get(section, {}) def get(self, section, option): return self.config.get(section, {}).get(option) class BaseTestWeb(ZuulTestCase): tenant_config_file = 'config/single-tenant/main.yaml' config_ini_data = {} def setUp(self): super(BaseTestWeb, self).setUp() self.executor_server.hold_jobs_in_build = True A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') A.addApproval('Code-Review', 2) self.fake_gerrit.addEvent(A.addApproval('Approved', 1)) B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B') B.addApproval('Code-Review', 2) self.fake_gerrit.addEvent(B.addApproval('Approved', 1)) self.waitUntilSettled() self.zuul_ini_config = FakeConfig(self.config_ini_data) # Start the web server self.web = zuul.web.ZuulWeb( listen_address='127.0.0.1', listen_port=0, gear_server='127.0.0.1', gear_port=self.gearman_server.port, info=zuul.model.WebInfo.fromConfig(self.zuul_ini_config) ) loop = asyncio.new_event_loop() loop.set_debug(True) ws_thread = threading.Thread(target=self.web.run, args=(loop,)) ws_thread.start() self.addCleanup(loop.close) self.addCleanup(ws_thread.join) self.addCleanup(self.web.stop) self.host = 'localhost' # Wait until web server is started while True: time.sleep(0.1) if self.web.server is None: continue self.port = self.web.server.sockets[0].getsockname()[1] print(self.host, self.port) try: with socket.create_connection((self.host, self.port)): break except ConnectionRefusedError: pass def tearDown(self): self.executor_server.hold_jobs_in_build = False self.executor_server.release() self.waitUntilSettled() super(BaseTestWeb, self).tearDown() class TestWeb(BaseTestWeb): def test_web_status(self): "Test that we can retrieve JSON status info" self.executor_server.hold_jobs_in_build = True A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') A.addApproval('Code-Review', 2) self.fake_gerrit.addEvent(A.addApproval('Approved', 1)) self.waitUntilSettled() self.executor_server.release('project-merge') self.waitUntilSettled() req = urllib.request.Request( "http://localhost:%s/tenant-one/status" % self.port) f = urllib.request.urlopen(req) headers = f.info() self.assertIn('Content-Length', headers) self.assertIn('Content-Type', headers) self.assertEqual( 'application/json; charset=utf-8', headers['Content-Type']) self.assertIn('Access-Control-Allow-Origin', headers) self.assertIn('Cache-Control', headers) self.assertIn('Last-Modified', headers) data = f.read().decode('utf8') self.executor_server.hold_jobs_in_build = False self.executor_server.release() self.waitUntilSettled() data = json.loads(data) status_jobs = [] for p in data['pipelines']: for q in p['change_queues']: if p['name'] in ['gate', 'conflict']: self.assertEqual(q['window'], 20) else: self.assertEqual(q['window'], 0) for head in q['heads']: for change in head: self.assertTrue(change['active']) self.assertIn(change['id'], ('1,1', '2,1', '3,1')) for job in change['jobs']: status_jobs.append(job) self.assertEqual('project-merge', status_jobs[0]['name']) # TODO(mordred) pull uuids from self.builds self.assertEqual( 'stream.html?uuid={uuid}&logfile=console.log'.format( uuid=status_jobs[0]['uuid']), status_jobs[0]['url']) self.assertEqual( 'finger://{hostname}/{uuid}'.format( hostname=self.executor_server.hostname, uuid=status_jobs[0]['uuid']), status_jobs[0]['finger_url']) # TOOD(mordred) configure a success-url on the base job self.assertEqual( 'finger://{hostname}/{uuid}'.format( hostname=self.executor_server.hostname, uuid=status_jobs[0]['uuid']), status_jobs[0]['report_url']) self.assertEqual('project-test1', status_jobs[1]['name']) self.assertEqual( 'stream.html?uuid={uuid}&logfile=console.log'.format( uuid=status_jobs[1]['uuid']), status_jobs[1]['url']) self.assertEqual( 'finger://{hostname}/{uuid}'.format( hostname=self.executor_server.hostname, uuid=status_jobs[1]['uuid']), status_jobs[1]['finger_url']) self.assertEqual( 'finger://{hostname}/{uuid}'.format( hostname=self.executor_server.hostname, uuid=status_jobs[1]['uuid']), status_jobs[1]['report_url']) self.assertEqual('project-test2', status_jobs[2]['name']) self.assertEqual( 'stream.html?uuid={uuid}&logfile=console.log'.format( uuid=status_jobs[2]['uuid']), status_jobs[2]['url']) self.assertEqual( 'finger://{hostname}/{uuid}'.format( hostname=self.executor_server.hostname, uuid=status_jobs[2]['uuid']), status_jobs[2]['finger_url']) self.assertEqual( 'finger://{hostname}/{uuid}'.format( hostname=self.executor_server.hostname, uuid=status_jobs[2]['uuid']), status_jobs[2]['report_url']) # check job dependencies self.assertIsNotNone(status_jobs[0]['dependencies']) self.assertIsNotNone(status_jobs[1]['dependencies']) self.assertIsNotNone(status_jobs[2]['dependencies']) self.assertEqual(len(status_jobs[0]['dependencies']), 0) self.assertEqual(len(status_jobs[1]['dependencies']), 1) self.assertEqual(len(status_jobs[2]['dependencies']), 1) self.assertIn('project-merge', status_jobs[1]['dependencies']) self.assertIn('project-merge', status_jobs[2]['dependencies']) def test_web_bad_url(self): # do we 404 correctly req = urllib.request.Request( "http://localhost:%s/status/foo" % self.port) self.assertRaises(urllib.error.HTTPError, urllib.request.urlopen, req) def test_web_find_change(self): # can we filter by change id req = urllib.request.Request( "http://localhost:%s/tenant-one/status/change/1,1" % self.port) f = urllib.request.urlopen(req) data = json.loads(f.read().decode('utf8')) self.assertEqual(1, len(data), data) self.assertEqual("org/project", data[0]['project']) req = urllib.request.Request( "http://localhost:%s/tenant-one/status/change/2,1" % self.port) f = urllib.request.urlopen(req) data = json.loads(f.read().decode('utf8')) self.assertEqual(1, len(data), data) self.assertEqual("org/project1", data[0]['project'], data) def test_web_keys(self): with open(os.path.join(FIXTURE_DIR, 'public.pem'), 'rb') as f: public_pem = f.read() req = urllib.request.Request( "http://localhost:%s/tenant-one/org/project.pub" % self.port) f = urllib.request.urlopen(req) self.assertEqual(f.read(), public_pem) def test_web_404_on_unknown_tenant(self): req = urllib.request.Request( "http://localhost:{}/non-tenant/status".format(self.port)) e = self.assertRaises( urllib.error.HTTPError, urllib.request.urlopen, req) self.assertEqual(404, e.code) class TestInfo(BaseTestWeb): def setUp(self): super(TestInfo, self).setUp() web_config = self.config_ini_data.get('web', {}) self.websocket_url = web_config.get('websocket_url') self.stats_url = web_config.get('stats_url') statsd_config = self.config_ini_data.get('statsd', {}) self.stats_prefix = statsd_config.get('prefix') def test_info(self): req = urllib.request.Request( "http://localhost:%s/info" % self.port) f = urllib.request.urlopen(req) info = json.loads(f.read().decode('utf8')) self.assertEqual( info, { "info": { "rest_api_url": None, "capabilities": { "job_history": False }, "stats": { "url": self.stats_url, "prefix": self.stats_prefix, "type": "graphite", }, "websocket_url": self.websocket_url, } }) def test_tenant_info(self): req = urllib.request.Request( "http://localhost:%s/tenant-one/info" % self.port) f = urllib.request.urlopen(req) info = json.loads(f.read().decode('utf8')) self.assertEqual( info, { "info": { "rest_api_url": None, "tenant": "tenant-one", "capabilities": { "job_history": False }, "stats": { "url": self.stats_url, "prefix": self.stats_prefix, "type": "graphite", }, "websocket_url": self.websocket_url, } }) class TestWebSocketInfo(TestInfo): config_ini_data = { 'web': { 'websocket_url': 'wss://ws.example.com' } } class TestGraphiteUrl(TestInfo): config_ini_data = { 'statsd': { 'prefix': 'example' }, 'web': { 'stats_url': 'https://graphite.example.com', } }