Merge lp:~cjwatson/launchpad/remove-txlongpoll into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18930
Proposed branch: lp:~cjwatson/launchpad/remove-txlongpoll
Merge into: lp:launchpad
Diff against target: 2646 lines (+12/-2250)
36 files modified
Makefile (+2/-2)
configs/development/launchpad-lazr.conf (+0/-5)
configs/development/local-launchpad-apache (+0/-1)
configs/testrunner/launchpad-lazr.conf (+0/-5)
constraints.txt (+0/-2)
database/sampledata/current-dev.sql (+1/-3)
lib/lp/app/javascript/longpoll.js (+0/-217)
lib/lp/app/javascript/tests/test_longpoll.html (+0/-47)
lib/lp/app/javascript/tests/test_longpoll.js (+0/-283)
lib/lp/app/longpoll/__init__.py (+0/-50)
lib/lp/app/longpoll/tests/test_longpoll.py (+0/-121)
lib/lp/app/templates/base-layout-macros.pt (+1/-9)
lib/lp/code/browser/branchmergeproposal.py (+1/-5)
lib/lp/code/browser/tests/test_branchmergeproposal.py (+1/-35)
lib/lp/code/javascript/branchmergeproposal.updater.js (+0/-263)
lib/lp/code/javascript/tests/test_branchmergeproposal.updater.html (+0/-98)
lib/lp/code/javascript/tests/test_branchmergeproposal.updater.js (+0/-162)
lib/lp/code/templates/branchmergeproposal-index.pt (+2/-22)
lib/lp/scripts/runlaunchpad.py (+1/-25)
lib/lp/scripts/tests/test_runlaunchpad.py (+1/-5)
lib/lp/services/config/schema-lazr.conf (+0/-10)
lib/lp/services/configure.zcml (+1/-2)
lib/lp/services/features/flags.py (+1/-8)
lib/lp/services/longpoll/adapters/event.py (+0/-71)
lib/lp/services/longpoll/adapters/storm.py (+0/-116)
lib/lp/services/longpoll/adapters/subscriber.py (+0/-58)
lib/lp/services/longpoll/adapters/tests/test_event.py (+0/-78)
lib/lp/services/longpoll/adapters/tests/test_storm.py (+0/-170)
lib/lp/services/longpoll/adapters/tests/test_subscriber.py (+0/-137)
lib/lp/services/longpoll/configure.zcml (+0/-15)
lib/lp/services/longpoll/interfaces.py (+0/-64)
lib/lp/services/longpoll/testing.py (+0/-57)
lib/lp/services/longpoll/tests/test_interfaces.py (+0/-33)
lib/lp/services/txlongpoll/server.py (+0/-31)
lib/lp/services/txlongpoll/tests/test_server.py (+0/-38)
setup.py (+0/-2)
To merge this branch: bzr merge lp:~cjwatson/launchpad/remove-txlongpoll
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+366122@code.launchpad.net

Commit message

Remove txlongpoll.

Description of the change

The Launchpad integration for this was never fully rolled out, is broken in some hard-to-fix ways, and is unlikely to be fixed.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'Makefile'
--- Makefile 2018-06-06 12:46:56 +0000
+++ Makefile 2019-04-16 14:35:10 +0000
@@ -284,7 +284,7 @@
284 bin/test -f $(TESTFLAGS) $(TESTOPTS)284 bin/test -f $(TESTFLAGS) $(TESTOPTS)
285285
286run: build inplace stop286run: build inplace stop
287 bin/run -r librarian,bing-webservice,memcached,rabbitmq,txlongpoll \287 bin/run -r librarian,bing-webservice,memcached,rabbitmq \
288 -i $(LPCONFIG)288 -i $(LPCONFIG)
289289
290run-testapp: LPCONFIG=testrunner-appserver290run-testapp: LPCONFIG=testrunner-appserver
@@ -303,7 +303,7 @@
303run_all: build inplace stop303run_all: build inplace stop
304 bin/run \304 bin/run \
305 -r librarian,sftp,forker,mailman,codebrowse,bing-webservice,\305 -r librarian,sftp,forker,mailman,codebrowse,bing-webservice,\
306 memcached,rabbitmq,txlongpoll -i $(LPCONFIG)306 memcached,rabbitmq -i $(LPCONFIG)
307307
308run_codebrowse: compile308run_codebrowse: compile
309 BZR_PLUGIN_PATH=bzrplugins $(PY) scripts/start-loggerhead.py309 BZR_PLUGIN_PATH=bzrplugins $(PY) scripts/start-loggerhead.py
310310
=== modified file 'configs/development/launchpad-lazr.conf'
--- configs/development/launchpad-lazr.conf 2018-06-14 16:28:45 +0000
+++ configs/development/launchpad-lazr.conf 2019-04-16 14:35:10 +0000
@@ -193,11 +193,6 @@
193store_search_url: https://api.snapcraft.io/193store_search_url: https://api.snapcraft.io/
194tools_source: deb http://ppa.launchpad.net/snappy-dev/snapcraft-daily/ubuntu %(series)s main194tools_source: deb http://ppa.launchpad.net/snappy-dev/snapcraft-daily/ubuntu %(series)s main
195195
196[txlongpoll]
197launch: True
198frontend_port: 22435
199uri: /+longpoll/
200
201[rosetta]196[rosetta]
202global_suggestions_enabled: True197global_suggestions_enabled: True
203generate_templates: True198generate_templates: True
204199
=== modified file 'configs/development/local-launchpad-apache'
--- configs/development/local-launchpad-apache 2017-01-10 17:26:29 +0000
+++ configs/development/local-launchpad-apache 2019-04-16 14:35:10 +0000
@@ -32,7 +32,6 @@
32 SSLCertificateKeyFile /etc/apache2/ssl/launchpad.key32 SSLCertificateKeyFile /etc/apache2/ssl/launchpad.key
3333
34 ProxyPreserveHost on34 ProxyPreserveHost on
35 ProxyPass /+longpoll/ http://localhost:22435/ retry=1
36 ProxyPass /+combo !35 ProxyPass /+combo !
37 ProxyPass / http://localhost:8086/ retry=136 ProxyPass / http://localhost:8086/ retry=1
3837
3938
=== modified file 'configs/testrunner/launchpad-lazr.conf'
--- configs/testrunner/launchpad-lazr.conf 2018-05-22 07:53:52 +0000
+++ configs/testrunner/launchpad-lazr.conf 2019-04-16 14:35:10 +0000
@@ -164,11 +164,6 @@
164password: none164password: none
165virtual_host: none165virtual_host: none
166166
167[txlongpoll]
168launch: False
169frontend_port: none
170uri: none
171
172[rosetta]167[rosetta]
173generate_templates: True168generate_templates: True
174169
175170
=== modified file 'constraints.txt'
--- constraints.txt 2019-04-08 07:13:18 +0000
+++ constraints.txt 2019-04-16 14:35:10 +0000
@@ -365,8 +365,6 @@
365Twisted[conch,tls]==18.4.0365Twisted[conch,tls]==18.4.0
366txAMQP==0.6.2366txAMQP==0.6.2
367txfixtures==0.4.2367txfixtures==0.4.2
368txlongpoll==0.2.12
369txlongpollfixture==0.1.3
370txpkgupload==0.2368txpkgupload==0.2
371typing==3.6.2369typing==3.6.2
372unittest2==1.1.0370unittest2==1.1.0
373371
=== modified file 'database/sampledata/current-dev.sql'
--- database/sampledata/current-dev.sql 2017-12-15 12:15:25 +0000
+++ database/sampledata/current-dev.sql 2019-04-16 14:35:10 +0000
@@ -1,4 +1,4 @@
1-- Copyright 2010-2017 Canonical Ltd. This software is licensed under the1-- Copyright 2010-2019 Canonical Ltd. This software is licensed under the
2-- GNU Affero General Public License version 3 (see the file LICENSE).2-- GNU Affero General Public License version 3 (see the file LICENSE).
3-- Created using pg_dump (PostgreSQL) 9.3.53-- Created using pg_dump (PostgreSQL) 9.3.5
44
@@ -3825,7 +3825,6 @@
3825ALTER TABLE featureflag DISABLE TRIGGER ALL;3825ALTER TABLE featureflag DISABLE TRIGGER ALL;
38263826
3827INSERT INTO featureflag (scope, priority, flag, value, date_modified) VALUES ('default', 0, 'js.combo_loader.enabled', 'true', '2012-05-18 07:34:39.239649');3827INSERT INTO featureflag (scope, priority, flag, value, date_modified) VALUES ('default', 0, 'js.combo_loader.enabled', 'true', '2012-05-18 07:34:39.239649');
3828INSERT INTO featureflag (scope, priority, flag, value, date_modified) VALUES ('default', 1, 'longpoll.merge_proposals.enabled', 'true', '2012-05-18 07:34:39.239649');
38293828
38303829
3831ALTER TABLE featureflag ENABLE TRIGGER ALL;3830ALTER TABLE featureflag ENABLE TRIGGER ALL;
@@ -3833,7 +3832,6 @@
38333832
3834ALTER TABLE featureflagchangelogentry DISABLE TRIGGER ALL;3833ALTER TABLE featureflagchangelogentry DISABLE TRIGGER ALL;
38353834
3836INSERT INTO featureflagchangelogentry (id, date_changed, diff, comment, person) VALUES (1, '2011-10-06 12:44:04.37357', '+longpoll.merge_proposals.enabled default 1 true', 'Enable long-poll for merge proposals in development.', 16);
38373835
38383836
3839ALTER TABLE featureflagchangelogentry ENABLE TRIGGER ALL;3837ALTER TABLE featureflagchangelogentry ENABLE TRIGGER ALL;
38403838
=== removed file 'lib/lp/app/javascript/longpoll.js'
--- lib/lp/app/javascript/longpoll.js 2017-07-20 13:29:41 +0000
+++ lib/lp/app/javascript/longpoll.js 1970-01-01 00:00:00 +0000
@@ -1,217 +0,0 @@
1/* Copyright 2011 Canonical Ltd. This software is licensed under the
2 * GNU Affero General Public License version 3 (see the file LICENSE).
3 *
4 * The Launchpad Longpoll module provides the functionnality to deal
5 * with longpolling on the JavaScript side.
6 *
7 * The module method setupLongPollManager is called in every template and
8 * the long poll machinery will only be started if LP.cache.longpoll
9 * is populated.
10 *
11 * Usually the only thing you will want to do to use the long polling feature
12 * is:
13 *
14 * a) to make sure LP.cache.longpoll is populated with 'key' and 'uri'.
15 *
16 * b) to create Javascript handlers for the events which will be fired:
17 * - event_key will be fired when the event on the server side is triggered
18 * (event_key being the name of the event on the server side).
19 * - see below for other events fired by the longpoll machinery.
20 *
21 * @module longpoll
22 */
23YUI.add('lp.app.longpoll', function(Y) {
24
25var namespace = Y.namespace('lp.app.longpoll');
26
27// Event fired when the long polling request starts.
28namespace.longpoll_start_event = 'lp.app.longpoll.start';
29
30// Event fired each time the long polling request fails (to connect or
31// to parse the returned result).
32namespace.longpoll_fail_event = 'lp.app.longpoll.failure';
33
34// Event fired when the delay between each failed connection is set to
35// a long delay (after MAX_SHORT_DELAY_FAILED_ATTEMPTS failed attempts).
36namespace.longpoll_longdelay = 'lp.app.longpoll.longdelay';
37
38// Event fired when the delay between each failed connection is set back
39// to a short delay.
40namespace.longpoll_shortdelay = 'lp.app.longpoll.shortdelay';
41
42namespace._manager = null;
43
44// After MAX_SHORT_DELAY_FAILED_ATTEMPTS failed connections (real failed
45// connections or connection getting an invalid return) separated
46// by SHORT_DELAY (millisec), wait LONG_DELAY (millisec) between
47// each failed connection.
48namespace.MAX_SHORT_DELAY_FAILED_ATTEMPTS = 5;
49namespace.SHORT_DELAY = 1000;
50namespace.LONG_DELAY = 3*60*1000;
51
52/**
53 *
54 * A Long Poll Manager creates and manages a long polling connexion
55 * to the server to fetch events. This class is not directly used
56 * but managed through 'setupLongPollManager' which creates and
57 * initialises a singleton LongPollManager.
58 *
59 * @class LongPollManager
60 */
61function LongPollManager(config) {
62 LongPollManager.superclass.constructor.apply(this, arguments);
63}
64
65LongPollManager.NAME = "longPollManager";
66
67Y.extend(LongPollManager, Y.Base, {
68 initializer : function(cfg) {
69 this._started = false;
70 this._failed_attempts = 0;
71 this._repoll = true;
72 this._sequence = 0;
73 },
74
75 setConnectionInfos : function(key, uri) {
76 this.key = key;
77 this.uri = uri;
78 },
79
80 _io : function (uri, config) {
81 Y.io(uri, config);
82 },
83
84 successPoll : function (id, response) {
85 try {
86 var data = Y.JSON.parse(response.responseText);
87 if (!data.hasOwnProperty('event_key')) {
88 throw new Error("Response has no event_key");
89 }
90 Y.fire(data.event_key, data);
91 return true;
92 }
93 catch (e) {
94 Y.fire(namespace.longpoll_fail_event, e);
95 return false;
96 }
97 },
98
99 failurePoll : function () {
100 Y.fire(namespace.longpoll_fail_event);
101 },
102
103 /**
104 * Return the delay (milliseconds) to wait before trying to reconnect
105 * again after a failed connection.
106 *
107 * The rationale here is that:
108 * 1. We should not try to reconnect instantaneously after a failed
109 * connection.
110 * 2. After a certain number of failed connections, we should set the
111 * delay between two failed connection to a bigger number because
112 * the server may be having problems.
113 *
114 * @method _pollDelay
115 */
116 _pollDelay : function() {
117 this._failed_attempts = this._failed_attempts + 1;
118 if (this._failed_attempts >=
119 namespace.MAX_SHORT_DELAY_FAILED_ATTEMPTS) {
120 Y.fire(namespace.longpoll_longdelay);
121 return namespace.LONG_DELAY;
122 }
123 else {
124 return namespace.SHORT_DELAY;
125 }
126 },
127
128 /**
129 * Relaunch a connection to the server after a successful or
130 * a failed connection.
131 *
132 * @method repoll
133 * @param {Boolean} failed: whether or not the previous connection
134 * has failed.
135 */
136 repoll : function(failed) {
137 if (!failed) {
138 if (this._failed_attempts >=
139 namespace.MAX_SHORT_DELAY_FAILED_ATTEMPTS) {
140 Y.fire(namespace.longpoll_shortdelay);
141 }
142 this._failed_attempts = 0;
143 if (this._repoll) {
144 this.poll();
145 }
146 }
147 else {
148 var delay = this._pollDelay();
149 if (this._repoll) {
150 Y.later(delay, this, this.poll);
151 }
152 }
153 },
154
155 poll : function() {
156 var that = this;
157 var config = {
158 method: "GET",
159 sync: false,
160 on: {
161 failure: function(id, response) {
162 if (Y.Lang.isValue(response) &&
163 Y.Lang.isValue(response.status) &&
164 (response.status === 408 ||
165 response.status === 504)) {
166 // If the error code is:
167 // - 408 Request timeout
168 // - 504 Gateway timeout
169 // Then ignore the error and start
170 // polling again.
171 that.repoll(false);
172 }
173 else {
174 that.failurePoll();
175 that.repoll(true);
176 }
177 },
178 success: function(id, response) {
179 var res = that.successPoll(id, response);
180 that.repoll(res);
181 }
182 }
183 };
184 this._sequence = this._sequence + 1;
185 var queue_uri = this.uri +
186 "?uuid=" + this.key +
187 "&sequence=" + this._sequence;
188 if (!this._started) {
189 Y.fire(namespace.longpoll_start_event);
190 this._started = true;
191 }
192 this._io(queue_uri, config);
193 }
194});
195
196namespace.LongPollManager = LongPollManager;
197
198namespace.getLongPollManager = function() {
199 if (!Y.Lang.isValue(namespace._manager)) {
200 namespace._manager = new namespace.LongPollManager();
201 }
202 return namespace._manager;
203};
204
205namespace.setupLongPollManager = function() {
206 if (Y.Object.owns(LP.cache, 'longpoll') &&
207 Y.Lang.isValue(LP.cache.longpoll)) {
208 var key = LP.cache.longpoll.key;
209 var uri = LP.cache.longpoll.uri;
210 var longpollmanager = namespace.getLongPollManager();
211 longpollmanager.setConnectionInfos(key, uri);
212 longpollmanager.poll();
213 return longpollmanager;
214 }
215};
216
217}, "0.1", {"requires":["base", "event", "json", "io"]});
2180
=== removed file 'lib/lp/app/javascript/tests/test_longpoll.html'
--- lib/lp/app/javascript/tests/test_longpoll.html 2012-10-26 09:54:28 +0000
+++ lib/lp/app/javascript/tests/test_longpoll.html 1970-01-01 00:00:00 +0000
@@ -1,47 +0,0 @@
1<!DOCTYPE html>
2<!--
3Copyright 2012 Canonical Ltd. This software is licensed under the
4GNU Affero General Public License version 3 (see the file LICENSE).
5-->
6
7<html>
8 <head>
9 <title>Test longpoll</title>
10
11 <!-- YUI and test setup -->
12 <script type="text/javascript"
13 src="../../../../../build/js/yui/yui/yui.js">
14 </script>
15 <link rel="stylesheet"
16 href="../../../../../build/js/yui/console/assets/console-core.css" />
17 <link rel="stylesheet"
18 href="../../../../../build/js/yui/test-console/assets/skins/sam/test-console.css" />
19 <link rel="stylesheet"
20 href="../../../../../build/js/yui/test/assets/skins/sam/test.css" />
21
22 <script type="text/javascript"
23 src="../../../../../build/js/lp/app/testing/testrunner.js"></script>
24
25 <link rel="stylesheet" href="../../../app/javascript/testing/test.css" />
26
27 <!-- Dependencies -->
28 <script type="text/javascript"
29 src="../../../../../build/js/lp/app/client.js"></script>
30
31 <!-- The module under test. -->
32 <script type="text/javascript" src="../longpoll.js"></script>
33
34 <!-- Placeholder for any css asset for this module. -->
35 <!-- <link rel="stylesheet" href="../assets/longpoll-core.css" /> -->
36
37 <!-- The test suite. -->
38 <script type="text/javascript" src="test_longpoll.js"></script>
39
40 </head>
41 <body class="yui3-skin-sam">
42 <ul id="suites">
43 <!-- <li>lp.large_indicator.test</li> -->
44 <li>lp.longpoll.test</li>
45 </ul>
46 </body>
47</html>
480
=== removed file 'lib/lp/app/javascript/tests/test_longpoll.js'
--- lib/lp/app/javascript/tests/test_longpoll.js 2017-07-24 15:37:03 +0000
+++ lib/lp/app/javascript/tests/test_longpoll.js 1970-01-01 00:00:00 +0000
@@ -1,283 +0,0 @@
1/* Copyright 2011 Canonical Ltd. This software is licensed under the
2 * GNU Affero General Public License version 3 (see the file LICENSE). */
3YUI.add('lp.longpoll.test', function (Y) {
4 var longpoll = Y.lp.app.longpoll;
5
6 var tests = Y.namespace('lp.longpoll.test');
7 tests.suite = new Y.Test.Suite('longpoll Tests');
8
9 tests.suite.add(new Y.Test.Case({
10 name: 'TestLongPollSingleton',
11 tearDown: function() {
12 // Cleanup the singleton;
13 longpoll._manager = null;
14 },
15
16 testGetSingletonLongPollManager: function() {
17 Y.Assert.isNull(longpoll._manager);
18 var manager = longpoll.getLongPollManager();
19 Y.Assert.isNotNull(longpoll._manager);
20 var manager2 = longpoll.getLongPollManager();
21 Y.Assert.areSame(manager, manager2);
22 },
23
24 testInitLongPollManagerNoLongPoll: function() {
25 // if LP.cache.longpoll.key is undefined: no longpoll manager
26 // is created by setupLongPollManager.
27 window.LP = {
28 links: {},
29 cache: {}
30 };
31
32 longpoll.setupLongPollManager(true);
33 Y.Assert.isNull(longpoll._manager);
34 },
35
36 testInitLongPollManagerLongPoll: function() {
37 window.LP = {
38 links: {},
39 cache: {
40 longpoll: {
41 key: 'key',
42 uri: '/+longpoll/'
43 }
44 }
45 };
46
47 longpoll.setupLongPollManager(true);
48 Y.Assert.isNotNull(longpoll._manager);
49 }
50 }));
51
52 tests.suite.add(new Y.Test.Case({
53 name: 'TestLongPoll',
54
55 setUp: function() {
56 var manager = longpoll.getLongPollManager();
57 manager._repoll = false;
58 this.createBaseLP();
59 },
60
61 tearDown: function() {
62 // Cleanup the singleton;
63 longpoll._manager = null;
64 },
65
66 createBaseLP:function() {
67 window.LP = {
68 links: {},
69 cache: {}
70 };
71 },
72
73 setupLPCache: function() {
74 LP.cache.longpoll = {
75 key: 'key',
76 uri: '/+longpoll/'
77 };
78 },
79
80 setupLongPoll: function(nb_calls) {
81 this.setupLPCache();
82 return longpoll.setupLongPollManager(true);
83 },
84
85 testInitLongPollManagerQueueName: function() {
86 var manager = this.setupLongPoll();
87 Y.Assert.areEqual(LP.cache.longpoll.key, manager.key);
88 Y.Assert.areEqual(LP.cache.longpoll.uri, manager.uri);
89 Y.Assert.isFalse(Y.Lang.isValue(manager.nb_calls));
90 },
91
92 testPollStarted: function() {
93 var fired = false;
94 Y.on(longpoll.longpoll_start_event, function() {
95 fired = true;
96 });
97 this.setupLongPoll();
98 Y.Assert.isTrue(fired, "Start event not fired.");
99 },
100
101 testPollFailure: function() {
102 var fired = false;
103 Y.on(longpoll.longpoll_fail_event, function() {
104 fired = true;
105 });
106 // Monkeypatch io to simulate failure.
107 var manager = longpoll.getLongPollManager();
108 manager._io = function(uri, config) {
109 config.on.failure();
110 };
111 this.setupLongPoll();
112 Y.Assert.isTrue(fired, "Failure event not fired.");
113 },
114
115 testSuccessPollInvalidData: function() {
116 var manager = longpoll.getLongPollManager();
117 var custom_response = "{{";
118 var response = {
119 responseText: custom_response
120 };
121 var res = manager.successPoll("2", response);
122 Y.Assert.isFalse(res);
123 },
124
125 testSuccessPollMalformedData: function() {
126 var manager = longpoll.getLongPollManager();
127 var response = {
128 responseText: '{ "something": "6" }'
129 };
130 var res = manager.successPoll("2", response);
131 Y.Assert.isFalse(res);
132 },
133
134 testSuccessPollWellformedData: function() {
135 var manager = longpoll.getLongPollManager();
136 var response = {
137 responseText: '{ "event_key": "4", "something": "6"}'
138 };
139 var res = manager.successPoll("2", response);
140 Y.Assert.isTrue(res);
141 },
142
143 testPollDelay: function() {
144 // Create event listeners.
145 var longdelay_event_fired = false;
146 Y.on(longpoll.longpoll_longdelay, function(data) {
147 longdelay_event_fired = true;
148 });
149 var shortdelay_event_fired = false;
150 Y.on(longpoll.longpoll_shortdelay, function(data) {
151 shortdelay_event_fired = true;
152 });
153 var manager = longpoll.getLongPollManager();
154 // Monkeypatch io to simulate failure.
155 manager._io = function(uri, config) {
156 config.on.failure();
157 };
158 Y.Assert.areEqual(0, manager._failed_attempts);
159 this.setupLongPoll();
160 Y.Assert.areEqual(1, manager._failed_attempts);
161 var i, delay;
162 for (i=0; i<longpoll.MAX_SHORT_DELAY_FAILED_ATTEMPTS-2; i++) {
163 Y.Assert.areEqual(i+1, manager._failed_attempts);
164 delay = manager._pollDelay();
165 Y.Assert.areEqual(delay, longpoll.SHORT_DELAY);
166 }
167 // After MAX_SHORT_DELAY_FAILED_ATTEMPTS failed attempts, the
168 // delay returned by _pollDelay is LONG_DELAY and
169 // longpoll_longdelay is fired.
170 Y.Assert.isFalse(longdelay_event_fired);
171 delay = manager._pollDelay();
172 Y.Assert.isTrue(longdelay_event_fired);
173 Y.Assert.areEqual(delay, longpoll.LONG_DELAY);
174
175 // Monkeypatch io to simulate success.
176 manager._io = function(uri, config) {
177 config.on.success();
178 };
179 // After a success, longpoll.longpoll_shortdelay is fired.
180 Y.Assert.isFalse(shortdelay_event_fired);
181 delay = manager.poll();
182 Y.Assert.isTrue(shortdelay_event_fired);
183 },
184
185 testPollUriSequence: function() {
186 // Each new polling increases the sequence parameter:
187 // /+longpoll/?uuid=key&sequence=1
188 // /+longpoll/?uuid=key&sequence=2
189 // /+longpoll/?uuid=key&sequence=3
190 // ..
191 var count = 0;
192 // Monkeypatch io to simulate failure.
193 var manager = longpoll.getLongPollManager();
194 manager._io = function(uri, config) {
195 Y.Assert.areEqual(
196 '/+longpoll/?uuid=key&sequence=' + (count+1),
197 uri);
198 count = count + 1;
199 var response = {
200 responseText: '{"i":2}'
201 };
202 config.on.success(2, response);
203 };
204 this.setupLongPoll();
205 var request;
206 for (request=1; request<10; request++) {
207 Y.Assert.isTrue(count === request, "Uri not requested.");
208 manager.poll();
209 }
210 },
211
212 _testDoesNotFail: function(error_code) {
213 // Assert that, when the longpoll request receives an error
214 // with code error_code, it is not treated as a failed
215 // connection attempt.
216 var manager = longpoll.getLongPollManager();
217 // Monkeypatch io to simulate a request timeout.
218 manager._io = function(uri, config) {
219 var response = {status: error_code};
220 config.on.failure(4, response);
221 };
222
223 Y.Assert.areEqual(0, manager._failed_attempts);
224 this.setupLongPoll();
225 Y.Assert.areEqual(0, manager._failed_attempts);
226 },
227
228 test408RequestTimeoutHandling: function() {
229 this._testDoesNotFail(408);
230 },
231
232 test504GatewayTimeoutHandling: function() {
233 this._testDoesNotFail(504);
234 },
235
236 testPollPayLoadBad: function() {
237 // If a non valid response is returned, longpoll_fail_event
238 // is fired.
239 var fired = false;
240 Y.on(longpoll.longpoll_fail_event, function() {
241 fired = true;
242 });
243 var manager = longpoll.getLongPollManager();
244 // Monkeypatch io.
245 manager._io = function(uri, config) {
246 var response = {
247 responseText: "{non valid json"
248 };
249 config.on.success(2, response);
250 };
251 this.setupLongPoll();
252 Y.Assert.isTrue(fired, "Failure event not fired.");
253 },
254
255 testPollPayLoadOk: function() {
256 // Create a valid message.
257 var custom_response = {
258 'event_key': 'my-event',
259 'something': {something_else: 1234}
260 };
261 var fired = false;
262 Y.on(custom_response.event_key, function(data) {
263 fired = true;
264 Y.Assert.areEqual(data, custom_response);
265 });
266 var manager = longpoll.getLongPollManager();
267 // Monkeypatch io.
268 manager._io = function(uri, config) {
269 var response = {
270 responseText: Y.JSON.stringify(custom_response)
271 };
272 config.on.success(2, response);
273 };
274 this.setupLongPoll();
275 Y.Assert.isTrue(fired, "Custom event not fired.");
276 }
277 }));
278
279}, '0.1', {
280 requires: [
281 'lp.testing.runner', 'test', 'test-console', 'node-event-simulate',
282 'json', 'lp.app.longpoll']
283});
2840
=== removed directory 'lib/lp/app/longpoll'
=== removed file 'lib/lp/app/longpoll/__init__.py'
--- lib/lp/app/longpoll/__init__.py 2012-02-21 22:46:28 +0000
+++ lib/lp/app/longpoll/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,50 +0,0 @@
1# Copyright 2011-2012 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Long-poll infrastructure."""
5
6__metaclass__ = type
7__all__ = [
8 "emit",
9 "subscribe",
10 ]
11
12from lazr.restful.utils import get_current_browser_request
13from zope.component import getAdapter
14
15from lp.services.longpoll.interfaces import (
16 ILongPollEvent,
17 ILongPollSubscriber,
18 )
19
20
21def subscribe(target, event_name=u"", request=None):
22 """Convenience method to subscribe the current request.
23
24 :param target: Something that can be adapted to `ILongPollEvent`.
25 :param event_name: The name of the event to subscribe to. This is used to
26 look up a named adapter from `target` to `ILongPollEvent`.
27 :param request: The request for which to get an `ILongPollSubscriber`. It
28 a request is not specified the currently active request is used.
29 :return: The `ILongPollEvent` that has been subscribed to.
30 """
31 event = getAdapter(target, ILongPollEvent, name=event_name)
32 if request is None:
33 request = get_current_browser_request()
34 subscriber = ILongPollSubscriber(request)
35 subscriber.subscribe(event)
36 return event
37
38
39def emit(source, event_name=u"", **data):
40 """Convenience method to emit a message for an event.
41
42 :param source: Something that can be adapted to `ILongPollEvent`.
43 :param event_name: The name of the event to subscribe to. This is used to
44 look up a named adapter from `target` to `ILongPollEvent`.
45 :param data: See `ILongPollEvent.emit`.
46 :return: The `ILongPollEvent` that has been emitted.
47 """
48 event = getAdapter(source, ILongPollEvent, name=event_name)
49 event.emit(**data)
50 return event
510
=== removed directory 'lib/lp/app/longpoll/tests'
=== removed file 'lib/lp/app/longpoll/tests/__init__.py'
=== removed file 'lib/lp/app/longpoll/tests/test_longpoll.py'
--- lib/lp/app/longpoll/tests/test_longpoll.py 2015-07-09 12:18:51 +0000
+++ lib/lp/app/longpoll/tests/test_longpoll.py 1970-01-01 00:00:00 +0000
@@ -1,121 +0,0 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for lp.app.longpoll."""
5
6__metaclass__ = type
7
8from zope.component import (
9 adapter,
10 getUtility,
11 )
12from zope.interface import (
13 Attribute,
14 implementer,
15 Interface,
16 )
17
18from lp.app.longpoll import (
19 emit,
20 subscribe,
21 )
22from lp.services.longpoll.interfaces import (
23 ILongPollEvent,
24 ILongPollSubscriber,
25 )
26from lp.services.messaging.interfaces import IMessageSession
27from lp.services.webapp.servers import LaunchpadTestRequest
28from lp.testing import TestCase
29from lp.testing.fixture import ZopeAdapterFixture
30from lp.testing.layers import LaunchpadFunctionalLayer
31
32
33class IFakeObject(Interface):
34
35 ident = Attribute("ident")
36
37
38@implementer(IFakeObject)
39class FakeObject:
40
41 def __init__(self, ident):
42 self.ident = ident
43
44
45@adapter(IFakeObject)
46@implementer(ILongPollEvent)
47class FakeEvent:
48
49 def __init__(self, source):
50 self.source = source
51
52 @property
53 def event_key(self):
54 return "event-key-%s" % self.source.ident
55
56 def emit(self, **data):
57 # Don't cargo-cult this; see .adapters.event.LongPollEvent instead.
58 session = getUtility(IMessageSession)
59 producer = session.getProducer(self.event_key)
60 producer.sendNow(data)
61
62
63class TestFunctions(TestCase):
64
65 layer = LaunchpadFunctionalLayer
66
67 def test_subscribe(self):
68 # subscribe() gets the ILongPollEvent for the given target and the
69 # ILongPollSubscriber for the given request (or the current request is
70 # discovered). It subscribes the latter to the event, then returns the
71 # event.
72 session = getUtility(IMessageSession)
73 request = LaunchpadTestRequest()
74 an_object = FakeObject(12345)
75 with ZopeAdapterFixture(FakeEvent):
76 event = subscribe(an_object, request=request)
77 self.assertIsInstance(event, FakeEvent)
78 self.assertEqual("event-key-12345", event.event_key)
79 session.flush()
80 # Emitting an event-key-12345 event will put something on the
81 # subscriber's queue.
82 event_data = {"1234": 5678}
83 event.emit(**event_data)
84 subscriber = ILongPollSubscriber(request)
85 subscribe_queue = session.getConsumer(subscriber.subscribe_key)
86 message = subscribe_queue.receive(timeout=5)
87 self.assertEqual(event_data, message)
88
89 def test_subscribe_to_named_event(self):
90 # When an event_name is given to subscribe(), a named adapter is used
91 # to get the ILongPollEvent for the given target.
92 request = LaunchpadTestRequest()
93 an_object = FakeObject(12345)
94 with ZopeAdapterFixture(FakeEvent, name="foo"):
95 event = subscribe(an_object, event_name="foo", request=request)
96 self.assertIsInstance(event, FakeEvent)
97
98 def test_emit(self):
99 # emit() gets the ILongPollEvent for the given target and passes the
100 # given data to its emit() method. It then returns the event.
101 an_object = FakeObject(12345)
102 with ZopeAdapterFixture(FakeEvent):
103 event = emit(an_object)
104 session = getUtility(IMessageSession)
105 producer = session.getProducer(event.event_key)
106 subscribe_queue = session.getConsumer("whatever")
107 producer.associateConsumerNow(subscribe_queue)
108 # Emit the event again; the subscribe queue was not associated
109 # with the event before now.
110 event_data = {"8765": 4321}
111 event = emit(an_object, **event_data)
112 message = subscribe_queue.receive(timeout=5)
113 self.assertEqual(event_data, message)
114
115 def test_emit_named_event(self):
116 # When an event_name is given to emit(), a named adapter is used to
117 # get the ILongPollEvent for the given target.
118 an_object = FakeObject(12345)
119 with ZopeAdapterFixture(FakeEvent, name="foo"):
120 event = emit(an_object, "foo")
121 self.assertIsInstance(event, FakeEvent)
1220
=== modified file 'lib/lp/app/templates/base-layout-macros.pt'
--- lib/lp/app/templates/base-layout-macros.pt 2019-01-05 09:54:44 +0000
+++ lib/lp/app/templates/base-layout-macros.pt 2019-04-16 14:35:10 +0000
@@ -120,7 +120,7 @@
120 //<![CDATA[120 //<![CDATA[
121 LPJS.use('base', 'node', 'console', 'event',121 LPJS.use('base', 'node', 'console', 'event',
122 'oop', 'lp', 'lp.app.foldables','lp.app.sorttable',122 'oop', 'lp', 'lp.app.foldables','lp.app.sorttable',
123 'lp.app.inlinehelp', 'lp.app.links', 'lp.app.longpoll',123 'lp.app.inlinehelp', 'lp.app.links',
124 'lp.bugs.bugtask_index', 'lp.bugs.subscribers',124 'lp.bugs.bugtask_index', 'lp.bugs.subscribers',
125 'lp.app.ellipsis', 'lp.code.branchmergeproposal.diff',125 'lp.app.ellipsis', 'lp.code.branchmergeproposal.diff',
126 'lp.views.global',126 'lp.views.global',
@@ -135,14 +135,6 @@
135 Y.lp.activate_collapsibles();135 Y.lp.activate_collapsibles();
136 Y.lp.app.foldables.activate();136 Y.lp.app.foldables.activate();
137 Y.lp.app.links.check_valid_lp_links();137 Y.lp.app.links.check_valid_lp_links();
138 // Longpolling will only start if
139 // LP.cache.longpoll is populated.
140 // We use Y.later to work around a Safari/Chrome 'feature':
141 // The mouse cursor stays 'busy' until all the requests started during
142 // page load are finished. Hence we want the long poll request to start
143 // right *after* the page has loaded.
144 Y.later(0, Y.lp.app.longpoll, Y.lp.app.longpoll.setupLongPollManager);
145
146 });138 });
147139
148 Y.on('lp:context:web_link:changed', function(e) {140 Y.on('lp:context:web_link:changed', function(e) {
149141
=== modified file 'lib/lp/code/browser/branchmergeproposal.py'
--- lib/lp/code/browser/branchmergeproposal.py 2019-01-31 14:45:32 +0000
+++ lib/lp/code/browser/branchmergeproposal.py 2019-04-16 14:35:10 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the1# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Views, navigation and actions for BranchMergeProposals."""4"""Views, navigation and actions for BranchMergeProposals."""
@@ -69,7 +69,6 @@
69 vocabulary_to_choice_edit_items,69 vocabulary_to_choice_edit_items,
70 )70 )
71from lp.app.browser.tales import DateTimeFormatterAPI71from lp.app.browser.tales import DateTimeFormatterAPI
72from lp.app.longpoll import subscribe
73from lp.code.adapters.branch import BranchMergeProposalNoPreviewDiffDelta72from lp.code.adapters.branch import BranchMergeProposalNoPreviewDiffDelta
74from lp.code.browser.codereviewcomment import CodeReviewDisplayComment73from lp.code.browser.codereviewcomment import CodeReviewDisplayComment
75from lp.code.browser.decorations import DecoratedBranch74from lp.code.browser.decorations import DecoratedBranch
@@ -628,9 +627,6 @@
628 else:627 else:
629 cache.objects['branch_diff_link'] = (628 cache.objects['branch_diff_link'] = (
630 canonical_url(self.context.parent) + '/+diff/')629 canonical_url(self.context.parent) + '/+diff/')
631 if getFeatureFlag("longpoll.merge_proposals.enabled"):
632 cache.objects['merge_proposal_event_key'] = subscribe(
633 self.context).event_key
634630
635 @action('Claim', name='claim')631 @action('Claim', name='claim')
636 def claim_action(self, action, data):632 def claim_action(self, action, data):
637633
=== modified file 'lib/lp/code/browser/tests/test_branchmergeproposal.py'
--- lib/lp/code/browser/tests/test_branchmergeproposal.py 2019-01-31 14:21:09 +0000
+++ lib/lp/code/browser/tests/test_branchmergeproposal.py 2019-04-16 14:35:10 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the1# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Unit tests for BranchMergeProposals."""4"""Unit tests for BranchMergeProposals."""
@@ -1576,26 +1576,6 @@
1576 git_api.notify(bmp.source_git_repository.getInternalPath()))1576 git_api.notify(bmp.source_git_repository.getInternalPath()))
1577 self.assertTrue(view.pending_diff)1577 self.assertTrue(view.pending_diff)
15781578
1579 def test_subscribe_to_merge_proposal_events_flag_disabled(self):
1580 # If the longpoll.merge_proposals.enabled flag is not enabled the user
1581 # is *not* subscribed to events relating to the merge proposal.
1582 bmp = self.factory.makeBranchMergeProposal()
1583 view = create_initialized_view(bmp, '+index', current_request=True)
1584 cache = IJSONRequestCache(view.request)
1585 self.assertNotIn("longpoll", cache.objects)
1586 self.assertNotIn("merge_proposal_event_key", cache.objects)
1587
1588 def test_subscribe_to_merge_proposal_events_flag_enabled(self):
1589 # If the longpoll.merge_proposals.enabled flag is enabled the user is
1590 # subscribed to events relating to the merge proposal.
1591 bmp = self.factory.makeBranchMergeProposal()
1592 self.useContext(feature_flags())
1593 set_feature_flag('longpoll.merge_proposals.enabled', 'enabled')
1594 view = create_initialized_view(bmp, '+index', current_request=True)
1595 cache = IJSONRequestCache(view.request)
1596 self.assertIn("longpoll", cache.objects)
1597 self.assertIn("merge_proposal_event_key", cache.objects)
1598
1599 def test_description_is_meta_description(self):1579 def test_description_is_meta_description(self):
1600 description = (1580 description = (
1601 "I'd like to make the bmp description appear as the meta "1581 "I'd like to make the bmp description appear as the meta "
@@ -2076,20 +2056,6 @@
2076 browser = self.getViewBrowser(bmp)2056 browser = self.getViewBrowser(bmp)
2077 assert 'unf_pbasyvpgf' in browser.contents2057 assert 'unf_pbasyvpgf' in browser.contents
20782058
2079 def test_pending_diff_message_with_longpoll_enabled(self):
2080 # If the longpoll feature flag is enabled then the message
2081 # displayed for a pending diff indicates that it'll update
2082 # automatically. See also
2083 # lib/lp/code/stories/branches/xx-branchmergeproposals.txt
2084 self.useContext(feature_flags())
2085 set_feature_flag('longpoll.merge_proposals.enabled', 'enabled')
2086 bmp = self.factory.makeBranchMergeProposal()
2087 browser = self.getViewBrowser(bmp)
2088 self.assertIn(
2089 "An updated diff is being calculated and will appear "
2090 "automatically when ready.",
2091 browser.contents)
2092
2093 def test_short_conversation_comments_not_truncated(self):2059 def test_short_conversation_comments_not_truncated(self):
2094 """Short comments should not be truncated."""2060 """Short comments should not be truncated."""
2095 comment = self.factory.makeCodeReviewComment(body='x y' * 100)2061 comment = self.factory.makeCodeReviewComment(body='x y' * 100)
20962062
=== removed file 'lib/lp/code/javascript/branchmergeproposal.updater.js'
--- lib/lp/code/javascript/branchmergeproposal.updater.js 2014-03-31 19:40:35 +0000
+++ lib/lp/code/javascript/branchmergeproposal.updater.js 1970-01-01 00:00:00 +0000
@@ -1,263 +0,0 @@
1/* Copyright 2011 Canonical Ltd. This software is licensed under the
2 * GNU Affero General Public License version 3 (see the file LICENSE).
3 *
4 * Code for updating the diff when a new version is available.
5 *
6 * @module lp.code.branchmergeproposal.updater
7 * @requires node, lp.client
8 */
9
10YUI.add('lp.code.branchmergeproposal.updater', function(Y) {
11
12var namespace = Y.namespace('lp.code.branchmergeproposal.updater');
13
14function UpdaterWidget(config) {
15 UpdaterWidget.superclass.constructor.apply(this, arguments);
16}
17
18Y.mix(UpdaterWidget, {
19
20 NAME: 'updaterWidget',
21
22 ATTRS: {
23
24 /**
25 * The LP client to use. If none is provided, one will be
26 * created during initialization.
27 *
28 * @attribute lp_client
29 */
30 lp_client: {
31 value: null
32 },
33
34 /**
35 * The summary node.
36 *
37 * @attribute summary_node
38 */
39 summary_node: {
40 value: null,
41 writeOnce: "initOnly"
42 },
43
44 /**
45 * Whether or not this MP is still 'pending'.
46 *
47 * @attribute pending
48 * @readOnly
49 */
50 pending: {
51 readOnly: true,
52 getter: function() {
53 return !Y.Lang.isValue(
54 this.get('srcNode').one('.diff-content'));
55 }
56 },
57
58 /**
59 * The HTML code for the stats diff.
60 *
61 * @attribute diff_stats
62 */
63 diff_stats: {
64 getter: function() {
65 var summary_node = this.get('summary_node');
66 if (!Y.Lang.isValue(summary_node) ||
67 !Y.Lang.isValue(summary_node.one(
68 '#summary-row-b-diff'))) {
69 return null;
70 }
71 return summary_node.one(
72 '#summary-row-b-diff').one('td').get('innerHTML');
73 },
74 setter: function(value) {
75 this._setup_diff_stats_container();
76 var container = this.get(
77 'summary_node').one('#summary-row-b-diff').one('td');
78 container.set('innerHTML', value);
79 }
80 }
81 }
82
83});
84
85Y.extend(UpdaterWidget, Y.Widget, {
86
87 /*
88 * The initializer method that is called from the base Plugin class.
89 *
90 * @method initializer
91 * @protected
92 */
93 initializer: function(cfg){
94 // If we have not been provided with a Launchpad Client, then
95 // create one now:
96 if (null === this.get("lp_client")){
97 // Create our own instance of the LP client.
98 this.set("lp_client", new Y.lp.client.Launchpad());
99 }
100 this.set('summary_node', cfg.summary_node);
101 },
102
103 /*
104 * Set the proper icon to indicate the diff is updating.
105 *
106 * @method set_status_updating
107 */
108 set_status_updating: function() {
109 this.cleanup_status();
110 this._set_status('spinner', 'Update in progress.');
111 },
112
113 /*
114 * Set the proper icon to indicate the diff will be updated when the
115 * new version is available.
116 *
117 * @method set_status_longpolling
118 */
119 set_status_longpolling: function() {
120 this.cleanup_status();
121 this._set_status(
122 'longpoll_loading',
123 'The diff will be updated as soon as a new version is available.');
124 },
125
126 /*
127 * Set the proper icon to indicate that the diff update is broken.
128 *
129 * @method set_status_longpollerror
130 */
131 set_status_longpollerror: function() {
132 this.cleanup_status();
133 this._set_status(
134 'longpoll_error',
135 'Diff update error, please reload to see the changes.');
136 },
137
138 /*
139 * Add a status image to the diff title.
140 *
141 * @method _set_status
142 */
143 _set_status: function(image_name, title) {
144 var image = Y.Node.create('<img />')
145 .set('src', '/@@/' + image_name)
146 .set('title', title);
147 this.get('srcNode').one('h2').append(image);
148 },
149
150 /*
151 * Remove the status image to the diff title.
152 *
153 * @method cleanup_status
154 */
155 cleanup_status: function() {
156 this._setup_diff_container();
157 this.get('srcNode').all('h2 img').remove();
158 },
159
160 /*
161 * Add a row in the page summary table to display the diff stats
162 * if needed.
163 *
164 * @method _setup_diff_stats_container
165 */
166 _setup_diff_stats_container: function() {
167 if (!Y.Lang.isValue(this.get('diff_stats'))) {
168 var summary_node = this.get('summary_node');
169 var diff_stats = Y.Node.create('<tr />')
170 .set('id', 'summary-row-b-diff')
171 .append(Y.Node.create('<th />')
172 .set("text", "Diff against target:"))
173 .append(Y.Node.create('<td />'));
174 summary_node.one(
175 '#summary-row-9-target-branch').insert(diff_stats, 'after');
176 }
177 },
178
179 /*
180 * Populate the widget with the required nodes to display the diff
181 * if needed.
182 *
183 * @method _setup_diff_container
184 */
185 _setup_diff_container: function() {
186 if (this.get('pending')) {
187 // Cleanup.get('srcNode').
188 this.get('srcNode').empty();
189 // Create the diff container.
190 var review_diff = Y.Node.create('<div />')
191 .set('id', 'review-diff')
192 .append(Y.Node.create('<h2 />')
193 .set("text", "Preview Diff "))
194 .append(Y.Node.create('<div />')
195 .addClass("diff-navigator"))
196 .append(Y.Node.create('<div />')
197 .addClass("diff-content"));
198 this.get('srcNode').append(review_diff);
199 }
200 },
201
202 /*
203 * Update the page diff stats.
204 *
205 * @method update
206 */
207 update: function() {
208 this.update_stats();
209 },
210
211 /*
212 * Update the diff stats with the last version.
213 *
214 * @method update_stats
215 */
216 update_stats: function() {
217 var self = this;
218 var config = {
219 on: {
220 success: function(diff_stats) {
221 self.set('diff_stats', diff_stats);
222 // (re)connect the js scroller link.
223 Y.lp.code.branchmergeproposal.reviewcomment.link_scroller(
224 '#proposal-summary a.diff-link', '#review-diff');
225 var node = self.get('summary_node');
226 Y.lp.anim.green_flash({node: node}).run();
227 self.fire(self.NAME + '.updated');
228 },
229 failure: function() {
230 var node = self.get('summary_node');
231 Y.lp.anim.red_flash({node: node}).run();
232 },
233 start: function() {
234 self.set_status_updating();
235 },
236 end: function() {
237 self.cleanup_status();
238 }
239 }
240 };
241 var mp_uri = LP.cache.context.web_link;
242 this.get('lp_client').get(mp_uri + "/++diff-stats", config);
243 }
244
245});
246
247/*
248 * Export UpdaterWidget.
249 */
250namespace.UpdaterWidget = UpdaterWidget;
251
252/*
253 * Returns true if the event fired means that the preview_diff field of the
254 * MP has been updated.
255 *
256 */
257namespace.is_mp_diff_updated = function(event_data) {
258 return (event_data.what === "modified" &&
259 event_data.edited_fields.indexOf("preview_diff") >= 0);
260};
261
262}, '0.1', {requires: ['node', 'lp.client', 'lp.anim',
263 'lp.code.branchmergeproposal.reviewcomment']});
2640
=== removed file 'lib/lp/code/javascript/tests/test_branchmergeproposal.updater.html'
--- lib/lp/code/javascript/tests/test_branchmergeproposal.updater.html 2012-10-26 09:54:28 +0000
+++ lib/lp/code/javascript/tests/test_branchmergeproposal.updater.html 1970-01-01 00:00:00 +0000
@@ -1,98 +0,0 @@
1<!DOCTYPE html>
2<!--
3Copyright 2012 Canonical Ltd. This software is licensed under the
4GNU Affero General Public License version 3 (see the file LICENSE).
5-->
6
7<html>
8 <head>
9 <title>Test branchmergeproposal</title>
10
11 <!-- YUI and test setup -->
12 <script type="text/javascript"
13 src="../../../../../build/js/yui/yui/yui.js">
14 </script>
15 <link rel="stylesheet"
16 href="../../../../../build/js/yui/console/assets/console-core.css" />
17 <link rel="stylesheet"
18 href="../../../../../build/js/yui/test-console/assets/skins/sam/test-console.css" />
19 <link rel="stylesheet"
20 href="../../../../../build/js/yui/test/assets/skins/sam/test.css" />
21
22 <script type="text/javascript"
23 src="../../../../../build/js/lp/app/testing/testrunner.js"></script>
24
25 <link rel="stylesheet" href="../../../app/javascript/testing/test.css" />
26
27 <!-- Dependencies -->
28 <script type="text/javascript"
29 src="../../../../../build/js/lp/..."></script>
30 <script type="text/javascript"
31 src="../../../../../build/js/lp/app/client.js"></script>
32 <script type="text/javascript"
33 src="../../../../../build/js/lp/app/lp.js"></script>
34 <script type="text/javascript"
35 src="../../../../../build/js/lp/app/anim/anim.js"></script>
36 <script type="text/javascript"
37 src="../../../../../build/js/lp/app/extras/extras.js"></script>
38 <script type="text/javascript"
39 src="../../../../../build/js/lp/app/testing/mockio.js"></script>
40 <script type="text/javascript"
41 src="../../../../../build/js/lp/code/branchmergeproposal.reviewcomment.js"></script>
42
43 <!-- The module under test. -->
44 <script type="text/javascript" src="../branchmergeproposal.updater.js"></script>
45
46 <!-- Any css assert for this module. -->
47 <!-- <link rel="stylesheet" href="../assets/branchmergeproposal-core.css" /> -->
48
49 <!-- The test suite. -->
50 <script type="text/javascript" src="test_branchmergeproposal.updater.js"></script>
51
52 <!-- expected variable -->
53 <script type="text/javascript">
54 var LP = {
55 cache: {},
56 links: {}
57 };
58 </script>
59
60 </head>
61 <body class="yui3-skin-sam">
62 <ul id="suites">
63 <!-- <li>lp.large_indicator.test</li> -->
64 <li>lp.branchmergeproposal.updater.test</li>
65 </ul>
66 <div id="placeholder" style="display:none;">
67 </div>
68
69 <script type="text/x-template" id="pending-mp">
70 <table id="proposal-summary">
71 <tr id="summary-row-9-target-branch">
72 <th>Merge into:</th>
73 <td>
74 <a href="/~me/project/branch"
75 class="sprite branch">lp://dev/~me/project/branch</a>
76 </td>
77 </tr>
78 </table>
79 <div id="diff-area">
80 <div class="pending-update" id="diff-pending-update">
81 <h3>Updating diff...</h3>
82 <p>An updated diff will be available in a few minutes.</p>
83 </div>
84 </div>
85 </script>
86
87 <script type="text/x-template" id="current-mp">
88 <div id="diff-area">
89 <div id="review-diff">
90 <h2>Preview Diff</h2>
91 <div class="diff-content">Example diff</div>
92 </div>
93 </div>
94 </script>
95
96
97 </body>
98</html>
990
=== removed file 'lib/lp/code/javascript/tests/test_branchmergeproposal.updater.js'
--- lib/lp/code/javascript/tests/test_branchmergeproposal.updater.js 2014-03-31 19:40:35 +0000
+++ lib/lp/code/javascript/tests/test_branchmergeproposal.updater.js 1970-01-01 00:00:00 +0000
@@ -1,162 +0,0 @@
1/* Copyright 2011 Canonical Ltd. This software is licensed under the
2 * GNU Affero General Public License version 3 (see the file LICENSE).
3 *
4 * Tests for lp.code.branchmergeproposal.updater.
5 *
6 */
7YUI.add('lp.branchmergeproposal.updater.test', function (Y) {
8var module = Y.lp.code.branchmergeproposal.updater;
9var UpdaterWidget = module.UpdaterWidget;
10
11
12var tests = Y.namespace('lp.branchmergeproposal.updater.test');
13tests.suite = new Y.Test.Suite("BranchMergeProposal Updater Tests");
14
15/*
16 * Tests for when the updater is built on top of a pending diff.
17 *
18 */
19
20var pending_mp = Y.one('#pending-mp').getContent();
21
22tests.suite.add(new Y.Test.Case({
23
24 name: 'branchmergeproposal-updater-pending-tests',
25
26 setUp: function() {
27 Y.one("#placeholder")
28 .empty()
29 .append(Y.Node.create(pending_mp));
30 var diff_area = Y.one('#diff-area');
31 var summary_node = Y.one('#proposal-summary');
32 this.updater = new UpdaterWidget(
33 {srcNode: diff_area, summary_node: summary_node});
34
35 LP.cache.context = {
36 web_link: "https://code.launchpad.dev/~foo/bar/foobr/+merge/123"};
37
38 },
39
40 tearDown: function() {
41 this.updater.destroy();
42 },
43
44 test_default_values: function() {
45 Y.Assert.isTrue(this.updater.get('pending'));
46 Y.Assert.isNull(this.updater.get('diff_stats'));
47 },
48
49 test__setup_diff_container: function() {
50 this.updater._setup_diff_container();
51 Y.Assert.isFalse(this.updater.get('pending'));
52 Y.Assert.areEqual(
53 "Preview Diff ",
54 this.updater.get(
55 'srcNode').one('#review-diff h2').get('text'));
56 Y.Assert.areEqual(
57 "",
58 this.updater.get(
59 'srcNode').one('.diff-content').get('text'));
60 },
61
62 test__setup_diff_stats_container: function() {
63 Y.Assert.isNull(this.updater.get('diff_stats'));
64 this.updater._setup_diff_stats_container();
65 Y.Assert.areEqual('', this.updater.get('diff_stats'));
66 },
67
68 test_set_diff_stats: function() {
69 this.updater.set('diff_stats', '13 lines (+4/-0) 1 file modified');
70 Y.Assert.areEqual(
71 '13 lines (+4/-0) 1 file modified',
72 this.updater.get('diff_stats'));
73 },
74
75 test_set_status_updating: function() {
76 this.updater.set_status_updating();
77 Y.Assert.areEqual(
78 '/@@/spinner',
79 Y.one('h2').one('img').getAttribute('src'));
80 },
81
82 test_set_status_longpolling: function() {
83 this.updater.set_status_longpolling();
84 Y.Assert.areEqual(
85 '/@@/longpoll_loading',
86 Y.one('h2').one('img').getAttribute('src'));
87 },
88
89 test_set_status_longpollerror: function() {
90 this.updater.set_status_longpollerror();
91 Y.Assert.areEqual(
92 '/@@/longpoll_error',
93 Y.one('h2').one('img').getAttribute('src'));
94 },
95
96 test_cleanup_status: function() {
97 this.updater._setup_diff_container();
98 this.updater.set_status_updating();
99 this.updater.cleanup_status();
100 Y.Assert.areEqual(
101 'Preview Diff ',
102 Y.one('h2').get('innerHTML'));
103 },
104
105 test_update_stats_success: function() {
106 var mockio = new Y.lp.testing.mockio.MockIo();
107 this.updater.get('lp_client').io_provider = mockio;
108 Y.Assert.isNull(this.updater.get('diff_stats'));
109 this.updater.update_stats();
110 mockio.success({
111 responseText: '13 lines (+4/-0) 1 file modified',
112 responseHeaders: {'Content-Type': 'text/html'}});
113
114 Y.Assert.areEqual(
115 '13 lines (+4/-0) 1 file modified',
116 this.updater.get('diff_stats'));
117 },
118
119 test_update_fires_event: function() {
120 var fired = false;
121 var mockio = new Y.lp.testing.mockio.MockIo();
122 this.updater.get('lp_client').io_provider = mockio;
123 this.updater.on(this.updater.NAME + '.updated', function() {
124 fired = true;
125 });
126 this.updater.update();
127 mockio.success({
128 responseText: '13 lines (+4/-0) 1 file modified',
129 responseHeaders: {'Content-Type': 'text/html'}});
130 Y.Assert.isTrue(fired);
131 }
132
133}));
134
135
136tests.suite.add(new Y.Test.Case({
137
138 name: 'branchmergeproposal-updater-utilities',
139
140 test_is_mp_diff_updated_modified: function() {
141 var data = {what: 'modified', edited_fields: ['preview_diff']};
142 Y.Assert.isTrue(module.is_mp_diff_updated(data));
143 },
144
145 test_is_mp_diff_updater_deleted: function() {
146 var data = {what: 'deleted'};
147 Y.Assert.isFalse(module.is_mp_diff_updated(data));
148 },
149
150 test_is_mp_diff_updated_title_changed: function() {
151 var data = {what: 'modified', edited_fields: ['title']};
152 Y.Assert.isFalse(module.is_mp_diff_updated(data));
153 }
154
155}));
156
157
158}, '0.1', {
159 requires: ['lp.testing.runner', 'test', 'dump', 'test-console', 'node',
160 'lp.testing.mockio', 'event',
161 'lp.code.branchmergeproposal.updater']
162});
1630
=== modified file 'lib/lp/code/templates/branchmergeproposal-index.pt'
--- lib/lp/code/templates/branchmergeproposal-index.pt 2019-01-31 13:48:34 +0000
+++ lib/lp/code/templates/branchmergeproposal-index.pt 2019-04-16 14:35:10 +0000
@@ -185,13 +185,10 @@
185 <div class="yui-g" tal:condition="python: not view.show_diff_update_link and view.pending_diff">185 <div class="yui-g" tal:condition="python: not view.show_diff_update_link and view.pending_diff">
186 <div class="pending-update" id="diff-pending-update">186 <div class="pending-update" id="diff-pending-update">
187 <h3>Updating diff...</h3>187 <h3>Updating diff...</h3>
188 <p tal:condition="not: features/longpoll.merge_proposals.enabled">188 <p>
189 An updated diff will be available in a few minutes. Reload to see the189 An updated diff will be available in a few minutes. Reload to see the
190 changes.190 changes.
191 </p>191 </p>
192 <p tal:condition="features/longpoll.merge_proposals.enabled">
193 An updated diff is being calculated and will appear automatically when ready.
194 </p>
195 </div>192 </div>
196 </div>193 </div>
197 <div class="yui-g" tal:condition="view/show_diff_update_link">194 <div class="yui-g" tal:condition="view/show_diff_update_link">
@@ -226,7 +223,7 @@
226 conf = <tal:status-config replace="view/status_config" />223 conf = <tal:status-config replace="view/status_config" />
227 LPJS.use('io-base', 'lp.code.branchmergeproposal.reviewcomment',224 LPJS.use('io-base', 'lp.code.branchmergeproposal.reviewcomment',
228 'lp.code.branchmergeproposal.status', 'lp.app.comment',225 'lp.code.branchmergeproposal.status', 'lp.app.comment',
229 'lp.code.branchmergeproposal.updater', 'lp.app.widgets.expander',226 'lp.app.widgets.expander',
230 'lp.code.branch.revisionexpander',227 'lp.code.branch.revisionexpander',
231 'lp.code.branchmergeproposal.inlinecomments', function(Y) {228 'lp.code.branchmergeproposal.inlinecomments', function(Y) {
232229
@@ -252,23 +249,6 @@
252 diffnav.render();249 diffnav.render();
253 }250 }
254251
255 if (Y.Lang.isValue(LP.cache.merge_proposal_event_key)) {
256 var upt = Y.lp.code.branchmergeproposal.updater;
257 var cfg = {
258 srcNode: Y.one('#diff-area'),
259 summary_node: Y.one('#proposal-summary')
260 };
261 var updater = new upt.UpdaterWidget(cfg);
262 Y.on(LP.cache.merge_proposal_event_key, function(data) {
263 if (upt.is_mp_diff_updated(data)) {
264 updater.update();
265 }
266 });
267 updater.on(updater.NAME + '.updated', function() {
268 diffnav.render();
269 });
270 }
271
272 LP.cache.comment_context = LP.cache.context;252 LP.cache.comment_context = LP.cache.context;
273 var cl = new Y.lp.app.comment.CommentList({253 var cl = new Y.lp.app.comment.CommentList({
274 comment_list_container: Y.one('#conversation')254 comment_list_container: Y.one('#conversation')
275255
=== modified file 'lib/lp/scripts/runlaunchpad.py'
--- lib/lp/scripts/runlaunchpad.py 2018-05-21 20:30:16 +0000
+++ lib/lp/scripts/runlaunchpad.py 2019-04-16 14:35:10 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the1# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -27,7 +27,6 @@
27 )27 )
28from lp.services.rabbit.server import RabbitServer28from lp.services.rabbit.server import RabbitServer
29from lp.services.sitesearch import bingtestservice29from lp.services.sitesearch import bingtestservice
30from lp.services.txlongpoll.server import TxLongPollServer
3130
3231
33def make_abspath(path):32def make_abspath(path):
@@ -237,28 +236,6 @@
237 self.useFixture(self.server)236 self.useFixture(self.server)
238237
239238
240class TxLongPollService(Service):
241 """A TxLongPoll service."""
242
243 @property
244 def should_launch(self):
245 return config.txlongpoll.launch
246
247 def launch(self):
248 twistd_bin = os.path.join(config.root, 'bin', 'twistd')
249 broker_hostname, broker_port = as_host_port(
250 config.rabbitmq.host, None, None)
251 self.server = TxLongPollServer(
252 twistd_bin=twistd_bin,
253 frontend_port=config.txlongpoll.frontend_port,
254 broker_user=config.rabbitmq.userid,
255 broker_password=config.rabbitmq.password,
256 broker_vhost=config.rabbitmq.virtual_host,
257 broker_host=broker_hostname,
258 broker_port=broker_port)
259 self.useFixture(self.server)
260
261
262def stop_process(process):239def stop_process(process):
263 """kill process and BLOCK until process dies.240 """kill process and BLOCK until process dies.
264241
@@ -284,7 +261,6 @@
284 'codebrowse': CodebrowseService(),261 'codebrowse': CodebrowseService(),
285 'memcached': MemcachedService(),262 'memcached': MemcachedService(),
286 'rabbitmq': RabbitService(),263 'rabbitmq': RabbitService(),
287 'txlongpoll': TxLongPollService(),
288 }264 }
289265
290266
291267
=== modified file 'lib/lp/scripts/tests/test_runlaunchpad.py'
--- lib/lp/scripts/tests/test_runlaunchpad.py 2018-05-21 20:30:16 +0000
+++ lib/lp/scripts/tests/test_runlaunchpad.py 2019-04-16 14:35:10 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the1# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for runlaunchpad.py"""4"""Tests for runlaunchpad.py"""
@@ -156,10 +156,6 @@
156 if config.rabbitmq.launch:156 if config.rabbitmq.launch:
157 expected.append(SERVICES['rabbitmq'])157 expected.append(SERVICES['rabbitmq'])
158158
159 # TxLongPoll may or may not be asked to run.
160 if config.txlongpoll.launch:
161 expected.append(SERVICES['txlongpoll'])
162
163 expected = sorted(expected)159 expected = sorted(expected)
164 self.assertEqual(expected, services)160 self.assertEqual(expected, services)
165161
166162
=== modified file 'lib/lp/services/config/schema-lazr.conf'
--- lib/lp/services/config/schema-lazr.conf 2019-03-26 20:51:38 +0000
+++ lib/lp/services/config/schema-lazr.conf 2019-04-16 14:35:10 +0000
@@ -1601,16 +1601,6 @@
1601# datatype: string1601# datatype: string
1602virtual_host: none1602virtual_host: none
16031603
1604[txlongpoll]
1605# Should TxLongPoll be launched by default?
1606# datatype: boolean
1607launch: False
1608# The port at which TxLongPoll is listening.
1609# datatype: string
1610frontend_port: none
1611# The uri that should be a proxy to the TxLongPoll server.
1612uri: /+longpoll/
1613
1614[request_daily_builds]1604[request_daily_builds]
1615dbuser: request-daily-builds1605dbuser: request-daily-builds
16161606
16171607
=== modified file 'lib/lp/services/configure.zcml'
--- lib/lp/services/configure.zcml 2018-03-16 14:50:01 +0000
+++ lib/lp/services/configure.zcml 2019-04-16 14:35:10 +0000
@@ -1,4 +1,4 @@
1<!-- Copyright 2010-2011 Canonical Ltd. This software is licensed under the1<!-- Copyright 2010-2019 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->3-->
44
@@ -16,7 +16,6 @@
16 <include package=".inlinehelp" file="meta.zcml" />16 <include package=".inlinehelp" file="meta.zcml" />
17 <include package=".job" />17 <include package=".job" />
18 <include package=".librarian" />18 <include package=".librarian" />
19 <include package=".longpoll" />
20 <include package=".mail" />19 <include package=".mail" />
21 <include package=".memcache" />20 <include package=".memcache" />
22 <include package=".messages" />21 <include package=".messages" />
2322
=== modified file 'lib/lp/services/features/flags.py'
--- lib/lp/services/features/flags.py 2018-05-21 20:30:16 +0000
+++ lib/lp/services/features/flags.py 2019-04-16 14:35:10 +0000
@@ -1,4 +1,4 @@
1# Copyright 2010-2018 Canonical Ltd. This software is licensed under the1# Copyright 2010-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__all__ = [4__all__ = [
@@ -171,13 +171,6 @@
171 'None are enabled',171 'None are enabled',
172 '',172 '',
173 ''),173 ''),
174 ('longpoll.merge_proposals.enabled',
175 'boolean',
176 ('Enables the longpoll mechanism for merge proposals so that diffs, '
177 'for example, are updated in-page when they are ready.'),
178 '',
179 '',
180 ''),
181 ('ajax.batch_navigator.enabled',174 ('ajax.batch_navigator.enabled',
182 'boolean',175 'boolean',
183 ('If true, batch navigators which have been wired to do so use ajax '176 ('If true, batch navigators which have been wired to do so use ajax '
184177
=== removed directory 'lib/lp/services/longpoll'
=== removed file 'lib/lp/services/longpoll/__init__.py'
=== removed directory 'lib/lp/services/longpoll/adapters'
=== removed file 'lib/lp/services/longpoll/adapters/__init__.py'
=== removed file 'lib/lp/services/longpoll/adapters/event.py'
--- lib/lp/services/longpoll/adapters/event.py 2015-07-09 12:18:51 +0000
+++ lib/lp/services/longpoll/adapters/event.py 1970-01-01 00:00:00 +0000
@@ -1,71 +0,0 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Long poll adapters."""
5
6__metaclass__ = type
7__all__ = [
8 "generate_event_key",
9 "LongPollEvent",
10 ]
11
12from zope.component import getUtility
13
14from lp.services.messaging.interfaces import IMessageSession
15
16
17def router_factory(event_key):
18 """Get a router for the given `event_key`."""
19 return getUtility(IMessageSession).getProducer(event_key)
20
21
22def generate_event_key(*components):
23 """Generate a suitable event name."""
24 if len(components) == 0:
25 raise AssertionError(
26 "Event keys must contain at least one component.")
27 return "longpoll.event.%s" % ".".join(
28 str(component) for component in components)
29
30
31class LongPollEvent:
32 """Base-class for event adapters.
33
34 Sub-classes need to define the `event_key` property and declare something
35 along the lines of::
36
37 @adapter(IAwesomeThing)
38 @implementer(ILongPollEvent)
39 class LongPollAwesomeThingEvent(LongPollEvent):
40 ...
41
42 Alternatively, use the `long_poll_event` class decorator::
43
44 @long_poll_event(IAwesomeThing)
45 class LongPollAwesomeThingEvent(LongPollEvent):
46 ...
47
48 In both cases the adapter should be registered in a `configure.zcml`
49 somewhere sensible::
50
51 <adapter factory=".adapters.LongPollAwesomeThingEvent" />
52
53 """
54
55 def __init__(self, source):
56 self.source = source
57
58 @property
59 def event_key(self):
60 """See `ILongPollEvent`."""
61 raise NotImplementedError(self.__class__.event_key)
62
63 def emit(self, **data):
64 """See `ILongPollEvent`.
65
66 The data will be updated with `event_key`, a copy of `self.event_key`.
67 """
68 event_key = self.event_key
69 data.update(event_key=event_key)
70 router = router_factory(event_key)
71 router.send(data)
720
=== removed file 'lib/lp/services/longpoll/adapters/storm.py'
--- lib/lp/services/longpoll/adapters/storm.py 2011-10-05 15:46:30 +0000
+++ lib/lp/services/longpoll/adapters/storm.py 1970-01-01 00:00:00 +0000
@@ -1,116 +0,0 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Long-poll life-cycle adapters."""
5
6from __future__ import absolute_import
7
8__metaclass__ = type
9__all__ = []
10
11from lazr.lifecycle.interfaces import (
12 IObjectCreatedEvent,
13 IObjectDeletedEvent,
14 IObjectModifiedEvent,
15 )
16from storm.base import Storm
17from storm.info import (
18 get_cls_info,
19 get_obj_info,
20 )
21from zope.component import adapter
22from zope.interface.interfaces import IAttribute
23from zope.security.proxy import removeSecurityProxy
24
25from lp.services.longpoll.adapters.event import (
26 generate_event_key,
27 LongPollEvent,
28 )
29from lp.services.longpoll.interfaces import (
30 ILongPollEvent,
31 long_poll_event,
32 )
33
34
35def gen_primary_key(model_instance):
36 """Generate the primary key values for the given model instance."""
37 cls_info = get_obj_info(model_instance).cls_info
38 for primary_key_column in cls_info.primary_key:
39 yield primary_key_column.__get__(model_instance)
40
41
42def get_primary_key(model_instance):
43 """Return the primary key for the given model instance.
44
45 If the primary key contains only one value it is returned, otherwise all
46 the primary key values are returned in a tuple.
47 """
48 pkey = tuple(gen_primary_key(model_instance))
49 return pkey[0] if len(pkey) == 1 else pkey
50
51
52@long_poll_event(Storm)
53class LongPollStormEvent(LongPollEvent):
54 """A `ILongPollEvent` for events of `Storm` objects.
55
56 This class knows how to construct a stable event key given a Storm object.
57 """
58
59 @property
60 def event_key(self):
61 """See `ILongPollEvent`.
62
63 Constructs the key from the table name and primary key values of the
64 Storm model object.
65 """
66 cls_info = get_obj_info(self.source).cls_info
67 return generate_event_key(
68 cls_info.table.name.lower(),
69 *gen_primary_key(self.source))
70
71
72@long_poll_event(type(Storm))
73class LongPollStormCreationEvent(LongPollEvent):
74 """A `ILongPollEvent` for events of `Storm` *classes*.
75
76 This class knows how to construct a stable event key given a Storm class.
77 """
78
79 @property
80 def event_key(self):
81 """See `ILongPollEvent`.
82
83 Constructs the key from the table name of the Storm class.
84 """
85 cls_info = get_cls_info(self.source)
86 return generate_event_key(
87 cls_info.table.name.lower())
88
89
90@adapter(Storm, IObjectCreatedEvent)
91def object_created(model_instance, object_event):
92 """Subscription handler for `Storm` creation events."""
93 model_class = removeSecurityProxy(model_instance).__class__
94 event = ILongPollEvent(model_class)
95 event.emit(what="created", id=get_primary_key(model_instance))
96
97
98@adapter(Storm, IObjectDeletedEvent)
99def object_deleted(model_instance, object_event):
100 """Subscription handler for `Storm` deletion events."""
101 event = ILongPollEvent(model_instance)
102 event.emit(what="deleted", id=get_primary_key(model_instance))
103
104
105@adapter(Storm, IObjectModifiedEvent)
106def object_modified(model_instance, object_event):
107 """Subscription handler for `Storm` modification events."""
108 edited_fields = object_event.edited_fields
109 if edited_fields is not None and len(edited_fields) != 0:
110 edited_field_names = sorted(
111 (field.__name__ if IAttribute.providedBy(field) else field)
112 for field in edited_fields)
113 event = ILongPollEvent(model_instance)
114 event.emit(
115 what="modified", edited_fields=edited_field_names,
116 id=get_primary_key(model_instance))
1170
=== removed file 'lib/lp/services/longpoll/adapters/subscriber.py'
--- lib/lp/services/longpoll/adapters/subscriber.py 2015-07-09 12:18:51 +0000
+++ lib/lp/services/longpoll/adapters/subscriber.py 1970-01-01 00:00:00 +0000
@@ -1,58 +0,0 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Long poll adapters."""
5
6__metaclass__ = type
7__all__ = [
8 "generate_subscribe_key",
9 "LongPollApplicationRequestSubscriber",
10 ]
11
12from uuid import uuid4
13
14from lazr.restful.interfaces import IJSONRequestCache
15from zope.component import (
16 adapter,
17 getUtility,
18 )
19from zope.interface import implementer
20from zope.publisher.interfaces import IApplicationRequest
21
22from lp.services.config import config
23from lp.services.longpoll.interfaces import ILongPollSubscriber
24from lp.services.messaging.interfaces import IMessageSession
25
26
27def generate_subscribe_key():
28 """Generate a suitable new, unique, subscribe key."""
29 return "longpoll.subscribe.%s" % uuid4()
30
31
32@adapter(IApplicationRequest)
33@implementer(ILongPollSubscriber)
34class LongPollApplicationRequestSubscriber:
35
36 def __init__(self, request):
37 self.request = request
38
39 @property
40 def subscribe_key(self):
41 objects = IJSONRequestCache(self.request).objects
42 if "longpoll" in objects:
43 return objects["longpoll"]["key"]
44 return None
45
46 def subscribe(self, event):
47 cache = IJSONRequestCache(self.request)
48 if "longpoll" not in cache.objects:
49 cache.objects["longpoll"] = {
50 "uri": config.txlongpoll.uri,
51 "key": generate_subscribe_key(),
52 "subscriptions": [],
53 }
54 session = getUtility(IMessageSession)
55 subscribe_queue = session.getConsumer(self.subscribe_key)
56 producer = session.getProducer(event.event_key)
57 producer.associateConsumer(subscribe_queue)
58 cache.objects["longpoll"]["subscriptions"].append(event.event_key)
590
=== removed directory 'lib/lp/services/longpoll/adapters/tests'
=== removed file 'lib/lp/services/longpoll/adapters/tests/__init__.py'
=== removed file 'lib/lp/services/longpoll/adapters/tests/test_event.py'
--- lib/lp/services/longpoll/adapters/tests/test_event.py 2015-07-08 16:05:11 +0000
+++ lib/lp/services/longpoll/adapters/tests/test_event.py 1970-01-01 00:00:00 +0000
@@ -1,78 +0,0 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Long-poll event adapter tests."""
5
6__metaclass__ = type
7
8from zope.interface import implementer
9
10from lp.services.longpoll.adapters.event import (
11 generate_event_key,
12 LongPollEvent,
13 )
14from lp.services.longpoll.interfaces import ILongPollEvent
15from lp.services.longpoll.testing import (
16 capture_longpoll_emissions,
17 LongPollEventRecord,
18 )
19from lp.testing import TestCase
20from lp.testing.layers import LaunchpadFunctionalLayer
21from lp.testing.matchers import Contains
22
23
24@implementer(ILongPollEvent)
25class FakeEvent(LongPollEvent):
26
27 @property
28 def event_key(self):
29 return "event-key-%s" % self.source
30
31
32class TestLongPollEvent(TestCase):
33
34 layer = LaunchpadFunctionalLayer
35
36 def test_interface(self):
37 event = FakeEvent("source")
38 self.assertProvides(event, ILongPollEvent)
39
40 def test_event_key(self):
41 # event_key is not implemented in LongPollEvent; subclasses must
42 # provide it.
43 event = LongPollEvent("source")
44 self.assertRaises(NotImplementedError, getattr, event, "event_key")
45
46 def test_emit(self):
47 # LongPollEvent.emit() sends the given data to `event_key`.
48 event = FakeEvent("source")
49 event_data = {"hello": 1234}
50 with capture_longpoll_emissions() as log:
51 event.emit(**event_data)
52 expected_message = LongPollEventRecord(
53 event_key=event.event_key,
54 data=dict(event_data, event_key=event.event_key))
55 self.assertThat(log, Contains(expected_message))
56
57
58class TestFunctions(TestCase):
59
60 def test_generate_event_key_no_components(self):
61 self.assertRaises(
62 AssertionError, generate_event_key)
63
64 def test_generate_event_key(self):
65 self.assertEqual(
66 "longpoll.event.event-name",
67 generate_event_key("event-name"))
68 self.assertEqual(
69 "longpoll.event.source-name.event-name",
70 generate_event_key("source-name", "event-name"))
71 self.assertEqual(
72 "longpoll.event.type-name.source-name.event-name",
73 generate_event_key("type-name", "source-name", "event-name"))
74
75 def test_generate_event_key_stringifies_components(self):
76 self.assertEqual(
77 "longpoll.event.job.1234.COMPLETED",
78 generate_event_key("job", 1234, "COMPLETED"))
790
=== removed file 'lib/lp/services/longpoll/adapters/tests/test_storm.py'
--- lib/lp/services/longpoll/adapters/tests/test_storm.py 2012-01-01 02:58:52 +0000
+++ lib/lp/services/longpoll/adapters/tests/test_storm.py 1970-01-01 00:00:00 +0000
@@ -1,170 +0,0 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Long-poll event adapter tests."""
5
6__metaclass__ = type
7
8from lazr.lifecycle.event import (
9 ObjectCreatedEvent,
10 ObjectDeletedEvent,
11 ObjectModifiedEvent,
12 )
13from storm.base import Storm
14from storm.properties import Int
15from zope.event import notify
16from zope.interface import Attribute
17
18from lp.services.longpoll.adapters.storm import (
19 gen_primary_key,
20 get_primary_key,
21 )
22from lp.services.longpoll.interfaces import ILongPollEvent
23from lp.services.longpoll.testing import (
24 capture_longpoll_emissions,
25 LongPollEventRecord,
26 )
27from lp.testing import TestCase
28from lp.testing.layers import LaunchpadFunctionalLayer
29from lp.testing.matchers import Provides
30
31
32class FakeStormClass(Storm):
33
34 __storm_table__ = 'FakeTable'
35
36 id = Int(primary=True)
37
38
39class FakeStormCompoundPrimaryKeyClass(Storm):
40
41 __storm_table__ = 'FakeTableWithCompoundPrimaryKey'
42 __storm_primary__ = 'id1', 'id2'
43
44 id1 = Int()
45 id2 = Int()
46
47
48class TestFunctions(TestCase):
49
50 def test_gen_primary_key(self):
51 # gen_primary_key() returns an iterable of values from the model
52 # instance's primary key.
53 storm_object = FakeStormClass()
54 storm_object.id = 1234
55 self.assertEqual([1234], list(gen_primary_key(storm_object)))
56
57 def test_gen_primary_key_compound_key(self):
58 # gen_primary_key() returns an iterable of values from the model
59 # instance's primary key.
60 storm_object = FakeStormCompoundPrimaryKeyClass()
61 storm_object.id1 = 1234
62 storm_object.id2 = 5678
63 self.assertEqual([1234, 5678], list(gen_primary_key(storm_object)))
64
65 def test_get_primary_key(self):
66 # get_primary_key() returns the value of the model instance's primary
67 # key.
68 storm_object = FakeStormClass()
69 storm_object.id = 1234
70 self.assertEqual(1234, get_primary_key(storm_object))
71
72 def test_get_primary_key_compound_key(self):
73 # get_primary_key() returns a tuple of all the values in the model
74 # instance's primary key when the model uses a compound primary key.
75 storm_object = FakeStormCompoundPrimaryKeyClass()
76 storm_object.id1 = 1234
77 storm_object.id2 = 5678
78 self.assertEqual((1234, 5678), get_primary_key(storm_object))
79
80
81class TestStormLifecycle(TestCase):
82
83 layer = LaunchpadFunctionalLayer
84
85 def test_storm_event_adapter(self):
86 storm_object = FakeStormClass()
87 storm_object.id = 1234
88 event = ILongPollEvent(storm_object)
89 self.assertThat(event, Provides(ILongPollEvent))
90 self.assertEqual(
91 "longpoll.event.faketable.1234",
92 event.event_key)
93
94 def test_storm_creation_event_adapter(self):
95 event = ILongPollEvent(FakeStormClass)
96 self.assertThat(event, Provides(ILongPollEvent))
97 self.assertEqual(
98 "longpoll.event.faketable",
99 event.event_key)
100
101 def test_storm_object_created(self):
102 storm_object = FakeStormClass()
103 storm_object.id = 1234
104 with capture_longpoll_emissions() as log:
105 notify(ObjectCreatedEvent(storm_object))
106 expected = LongPollEventRecord(
107 "longpoll.event.faketable", {
108 "event_key": "longpoll.event.faketable",
109 "what": "created",
110 "id": 1234,
111 })
112 self.assertEqual([expected], log)
113
114 def test_storm_object_deleted(self):
115 storm_object = FakeStormClass()
116 storm_object.id = 1234
117 with capture_longpoll_emissions() as log:
118 notify(ObjectDeletedEvent(storm_object))
119 expected = LongPollEventRecord(
120 "longpoll.event.faketable.1234", {
121 "event_key": "longpoll.event.faketable.1234",
122 "what": "deleted",
123 "id": 1234,
124 })
125 self.assertEqual([expected], log)
126
127 def test_storm_object_modified(self):
128 storm_object = FakeStormClass()
129 storm_object.id = 1234
130 with capture_longpoll_emissions() as log:
131 object_event = ObjectModifiedEvent(
132 storm_object, storm_object, ("itchy", "scratchy"))
133 notify(object_event)
134 expected = LongPollEventRecord(
135 "longpoll.event.faketable.1234", {
136 "event_key": "longpoll.event.faketable.1234",
137 "what": "modified",
138 "edited_fields": ["itchy", "scratchy"],
139 "id": 1234,
140 })
141 self.assertEqual([expected], log)
142
143 def test_storm_object_modified_no_edited_fields(self):
144 # A longpoll event is not emitted unless edited_fields is populated.
145 storm_object = FakeStormClass()
146 storm_object.id = 1234
147 with capture_longpoll_emissions() as log:
148 notify(ObjectModifiedEvent(storm_object, storm_object, None))
149 self.assertEqual([], log)
150 with capture_longpoll_emissions() as log:
151 notify(ObjectModifiedEvent(storm_object, storm_object, ()))
152 self.assertEqual([], log)
153
154 def test_storm_object_modified_edited_fields_are_zope_attributes(self):
155 # The names of IAttribute fields in edited_fields are used in the
156 # longpoll event.
157 storm_object = FakeStormClass()
158 storm_object.id = 1234
159 with capture_longpoll_emissions() as log:
160 object_event = ObjectModifiedEvent(
161 storm_object, storm_object, ("foo", Attribute("bar")))
162 notify(object_event)
163 expected = LongPollEventRecord(
164 "longpoll.event.faketable.1234", {
165 "event_key": "longpoll.event.faketable.1234",
166 "what": "modified",
167 "edited_fields": ["bar", "foo"],
168 "id": 1234,
169 })
170 self.assertEqual([expected], log)
1710
=== removed file 'lib/lp/services/longpoll/adapters/tests/test_subscriber.py'
--- lib/lp/services/longpoll/adapters/tests/test_subscriber.py 2015-07-08 16:05:11 +0000
+++ lib/lp/services/longpoll/adapters/tests/test_subscriber.py 1970-01-01 00:00:00 +0000
@@ -1,137 +0,0 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Long-poll subscriber adapter tests."""
5
6__metaclass__ = type
7
8from itertools import count
9
10from lazr.restful.interfaces import IJSONRequestCache
11from testtools.matchers import (
12 Not,
13 StartsWith,
14 )
15from zope.component import getUtility
16from zope.interface import implementer
17
18from lp.services.longpoll.adapters.subscriber import (
19 generate_subscribe_key,
20 LongPollApplicationRequestSubscriber,
21 )
22from lp.services.longpoll.interfaces import (
23 ILongPollEvent,
24 ILongPollSubscriber,
25 )
26from lp.services.messaging.interfaces import IMessageSession
27from lp.services.webapp.servers import LaunchpadTestRequest
28from lp.testing import TestCase
29from lp.testing.layers import LaunchpadFunctionalLayer
30from lp.testing.matchers import Contains
31
32
33@implementer(ILongPollEvent)
34class FakeEvent:
35
36 event_key_indexes = count(1)
37
38 def __init__(self):
39 self.event_key = "event-key-%d" % next(self.event_key_indexes)
40
41
42class TestLongPollSubscriber(TestCase):
43
44 layer = LaunchpadFunctionalLayer
45
46 def test_interface(self):
47 request = LaunchpadTestRequest()
48 subscriber = LongPollApplicationRequestSubscriber(request)
49 self.assertProvides(subscriber, ILongPollSubscriber)
50
51 def test_subscribe_key(self):
52 request = LaunchpadTestRequest()
53 subscriber = LongPollApplicationRequestSubscriber(request)
54 # A subscribe key is not generated yet.
55 self.assertIs(subscriber.subscribe_key, None)
56 # It it only generated on the first subscription.
57 subscriber.subscribe(FakeEvent())
58 subscribe_key = subscriber.subscribe_key
59 self.assertIsInstance(subscribe_key, str)
60 self.assertNotEqual(0, len(subscribe_key))
61 # It remains the same for later subscriptions.
62 subscriber.subscribe(FakeEvent())
63 self.assertEqual(subscribe_key, subscriber.subscribe_key)
64
65 def test_adapter(self):
66 request = LaunchpadTestRequest()
67 subscriber = ILongPollSubscriber(request)
68 self.assertIsInstance(
69 subscriber, LongPollApplicationRequestSubscriber)
70 # A difference subscriber is returned on subsequent adaptions, but it
71 # has the same subscribe_key.
72 subscriber2 = ILongPollSubscriber(request)
73 self.assertIsNot(subscriber, subscriber2)
74 self.assertEqual(subscriber.subscribe_key, subscriber2.subscribe_key)
75
76 def test_subscribe_queue(self):
77 # LongPollApplicationRequestSubscriber.subscribe() creates a new queue
78 # with a new unique name that is bound to the event's event_key.
79 request = LaunchpadTestRequest()
80 event = FakeEvent()
81 subscriber = ILongPollSubscriber(request)
82 subscriber.subscribe(event)
83 message = '{"hello": 1234}'
84 session = getUtility(IMessageSession)
85 routing_key = session.getProducer(event.event_key)
86 routing_key.send(message)
87 session.flush()
88 subscribe_queue = session.getConsumer(subscriber.subscribe_key)
89 self.assertEqual(
90 message, subscribe_queue.receive(timeout=5))
91
92 def test_json_cache_not_populated_on_init(self):
93 # LongPollApplicationRequestSubscriber does not put the name of the
94 # new queue into the JSON cache.
95 request = LaunchpadTestRequest()
96 cache = IJSONRequestCache(request)
97 self.assertThat(cache.objects, Not(Contains("longpoll")))
98 ILongPollSubscriber(request)
99 self.assertThat(cache.objects, Not(Contains("longpoll")))
100
101 def test_longpoll_uri_config(self):
102 # The JSON cache contains config.txlongpoll.uri.
103 self.pushConfig("txlongpoll", uri="/+longpoll/")
104 request = LaunchpadTestRequest()
105 cache = IJSONRequestCache(request)
106 ILongPollSubscriber(request).subscribe(FakeEvent())
107 self.assertEqual('/+longpoll/', cache.objects["longpoll"]["uri"])
108
109 def test_json_cache_populated_on_subscribe(self):
110 # To aid with debugging the event_key of subscriptions are added to
111 # the JSON cache.
112 request = LaunchpadTestRequest()
113 cache = IJSONRequestCache(request)
114 event1 = FakeEvent()
115 ILongPollSubscriber(request).subscribe(event1) # Side-effects!
116 self.assertThat(cache.objects, Contains("longpoll"))
117 self.assertThat(cache.objects["longpoll"], Contains("key"))
118 self.assertThat(cache.objects["longpoll"], Contains("subscriptions"))
119 self.assertEqual(
120 [event1.event_key],
121 cache.objects["longpoll"]["subscriptions"])
122 # More events can be subscribed.
123 event2 = FakeEvent()
124 ILongPollSubscriber(request).subscribe(event2)
125 self.assertEqual(
126 [event1.event_key, event2.event_key],
127 cache.objects["longpoll"]["subscriptions"])
128
129
130class TestFunctions(TestCase):
131
132 def test_generate_subscribe_key(self):
133 subscribe_key = generate_subscribe_key()
134 expected_prefix = "longpoll.subscribe."
135 self.assertThat(subscribe_key, StartsWith(expected_prefix))
136 # The key contains a 36 character UUID.
137 self.assertEqual(len(expected_prefix) + 36, len(subscribe_key))
1380
=== removed file 'lib/lp/services/longpoll/configure.zcml'
--- lib/lp/services/longpoll/configure.zcml 2011-10-05 15:14:53 +0000
+++ lib/lp/services/longpoll/configure.zcml 1970-01-01 00:00:00 +0000
@@ -1,15 +0,0 @@
1<!-- Copyright 2011 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->
4<configure
5 xmlns="http://namespaces.zope.org/zope"
6 xmlns:browser="http://namespaces.zope.org/browser"
7 xmlns:i18n="http://namespaces.zope.org/i18n"
8 i18n_domain="launchpad">
9 <adapter factory=".adapters.storm.LongPollStormEvent" />
10 <adapter factory=".adapters.storm.LongPollStormCreationEvent" />
11 <adapter factory=".adapters.subscriber.LongPollApplicationRequestSubscriber" />
12 <subscriber handler=".adapters.storm.object_created" />
13 <subscriber handler=".adapters.storm.object_deleted" />
14 <subscriber handler=".adapters.storm.object_modified" />
15</configure>
160
=== removed file 'lib/lp/services/longpoll/interfaces.py'
--- lib/lp/services/longpoll/interfaces.py 2011-09-20 19:04:19 +0000
+++ lib/lp/services/longpoll/interfaces.py 1970-01-01 00:00:00 +0000
@@ -1,64 +0,0 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Long-poll infrastructure interfaces."""
5
6__metaclass__ = type
7__all__ = [
8 "ILongPollEvent",
9 "ILongPollSubscriber",
10 "long_poll_event",
11 ]
12
13from zope.component import adapter
14from zope.interface import (
15 Attribute,
16 classImplements,
17 Interface,
18 )
19
20
21class ILongPollEvent(Interface):
22
23 source = Attribute("The event source.")
24
25 event_key = Attribute(
26 "The key with which events will be emitted. Should be predictable "
27 "and stable.")
28
29 def emit(**data):
30 """Emit the given data to `event_key`.
31
32 :param data: Any data structures that can be dumped as JSON.
33 """
34
35
36class ILongPollSubscriber(Interface):
37
38 subscribe_key = Attribute(
39 "The key which the subscriber must know in order to be able "
40 "to long-poll for subscribed events. Should be infeasible to "
41 "guess, a UUID for example.")
42
43 def subscribe(event):
44 """Subscribe to the given event.
45
46 :type event: ILongPollEvent
47 """
48
49
50def long_poll_event(source_spec):
51 """Class decorator to declare an `ILongPollEvent`.
52
53 :param source_spec: An interface or other specification understood by
54 `zope.component` (a plain class can be passed too) that defines the
55 source of an event. `IJob` or `storm.base.Storm` for example.
56 """
57 declare_adapter = adapter(source_spec)
58
59 def declare_event(cls):
60 classImplements(cls, ILongPollEvent)
61 declare_adapter(cls)
62 return cls
63
64 return declare_event
650
=== removed file 'lib/lp/services/longpoll/testing.py'
--- lib/lp/services/longpoll/testing.py 2011-09-23 16:36:56 +0000
+++ lib/lp/services/longpoll/testing.py 1970-01-01 00:00:00 +0000
@@ -1,57 +0,0 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Things that help with testing of longpoll."""
5
6__metaclass__ = type
7__all__ = [
8 "capture_longpoll_emissions",
9 "LongPollEventRecord",
10 ]
11
12from collections import namedtuple
13from contextlib import contextmanager
14from functools import partial
15
16from lp.services.longpoll.adapters import event
17
18
19LongPollEventRecord = namedtuple(
20 "LongPollEventRecord", ("event_key", "data"))
21
22
23class LoggingRouter:
24 """A test double for `IMessageProducer`.
25
26 Saves messages as `LongPollEventRecord` tuples to a log.
27
28 :param log: A callable accepting a single `LongPollEventRecord`.
29 :param routing_key: See `IMessageSession.getProducer`.
30 """
31
32 def __init__(self, log, routing_key):
33 self.log = log
34 self.routing_key = routing_key
35
36 def send(self, data):
37 record = LongPollEventRecord(self.routing_key, data)
38 self.log(record)
39
40
41@contextmanager
42def capture_longpoll_emissions():
43 """Capture longpoll emissions while this context is in force.
44
45 This returns a list in which `LongPollEventRecord` tuples will be
46 recorded, in the order they're emitted.
47
48 Note that normal event emission is *suppressed globally* while this
49 context is in force; *all* events will be stored in the log.
50 """
51 log = []
52 original_router_factory = event.router_factory
53 event.router_factory = partial(LoggingRouter, log.append)
54 try:
55 yield log
56 finally:
57 event.router_factory = original_router_factory
580
=== removed directory 'lib/lp/services/longpoll/tests'
=== removed file 'lib/lp/services/longpoll/tests/__init__.py'
=== removed file 'lib/lp/services/longpoll/tests/test_interfaces.py'
--- lib/lp/services/longpoll/tests/test_interfaces.py 2011-09-20 19:04:19 +0000
+++ lib/lp/services/longpoll/tests/test_interfaces.py 1970-01-01 00:00:00 +0000
@@ -1,33 +0,0 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Long-poll interface tests."""
5
6__metaclass__ = type
7
8from zope.component import adaptedBy
9from zope.interface import Interface
10
11from lp.services.longpoll.interfaces import (
12 ILongPollEvent,
13 long_poll_event,
14 )
15from lp.testing import TestCase
16
17
18class IEventSourceInterface(Interface):
19 """Test interface for an event source."""
20
21
22class TestLongPollInterfaces(TestCase):
23
24 def test_long_poll_event(self):
25 # long_poll_event is a class decorator that declares a class as an
26 # ILongPollEvent.
27 @long_poll_event(IEventSourceInterface)
28 class Something:
29 """An example event source."""
30 self.assertTrue(ILongPollEvent.implementedBy(Something))
31 self.assertEqual(
32 (IEventSourceInterface,),
33 adaptedBy(Something))
340
=== removed directory 'lib/lp/services/txlongpoll'
=== removed file 'lib/lp/services/txlongpoll/__init__.py'
=== removed file 'lib/lp/services/txlongpoll/server.py'
--- lib/lp/services/txlongpoll/server.py 2011-09-30 07:28:45 +0000
+++ lib/lp/services/txlongpoll/server.py 1970-01-01 00:00:00 +0000
@@ -1,31 +0,0 @@
1# Copyright 2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""TxLongPoll server fixture."""
5
6__metaclass__ = type
7__all__ = [
8 'TxLongPollServer',
9 ]
10
11from textwrap import dedent
12
13from txlongpollfixture.server import TxLongPollFixture
14
15
16class TxLongPollServer(TxLongPollFixture):
17 """A TxLongPoll server fixture with Launchpad-specific config.
18
19 :ivar service_config: A snippet of .ini that describes the `txlongpoll`
20 configuration.
21 """
22
23 def setUp(self):
24 super(TxLongPollServer, self).setUp()
25 setattr(
26 self, 'service_config',
27 dedent("""\
28 [txlongpoll]
29 frontend_port: %d
30 """ % (
31 self.config.frontend_port)))
320
=== removed directory 'lib/lp/services/txlongpoll/tests'
=== removed file 'lib/lp/services/txlongpoll/tests/__init__.py'
=== removed file 'lib/lp/services/txlongpoll/tests/test_server.py'
--- lib/lp/services/txlongpoll/tests/test_server.py 2017-09-23 03:13:41 +0000
+++ lib/lp/services/txlongpoll/tests/test_server.py 1970-01-01 00:00:00 +0000
@@ -1,38 +0,0 @@
1# Copyright 2011-2017 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for lp.services.rabbit.TxLongPollServer."""
5
6__metaclass__ = type
7
8from ConfigParser import SafeConfigParser
9import os
10from StringIO import StringIO
11
12from lp.services.config import config
13from lp.services.txlongpoll.server import TxLongPollServer
14from lp.testing import TestCase
15from lp.testing.layers import RabbitMQLayer
16
17
18class TestTxLongPollServer(TestCase):
19
20 layer = RabbitMQLayer
21
22 def test_service_config(self):
23 # TxLongPollServer pokes some .ini configuration into its
24 # service_config attributes.
25 twistd_bin = os.path.join(config.root, 'bin', 'twistd')
26 fixture = self.useFixture(TxLongPollServer(
27 broker_user='guest', broker_password='guest', broker_vhost='/',
28 broker_port=123, frontend_port=None,
29 twistd_bin=twistd_bin))
30 service_config = SafeConfigParser()
31 service_config.readfp(StringIO(getattr(fixture, 'service_config')))
32 self.assertEqual(["txlongpoll"], service_config.sections())
33 # txlongpoll section
34 expected = {
35 "frontend_port": "%d" % fixture.config.frontend_port,
36 }
37 observed = dict(service_config.items("txlongpoll"))
38 self.assertEqual(expected, observed)
390
=== modified file 'setup.py'
--- setup.py 2018-07-16 10:51:04 +0000
+++ setup.py 2019-04-16 14:35:10 +0000
@@ -227,8 +227,6 @@
227 'treq',227 'treq',
228 'Twisted[conch,tls]',228 'Twisted[conch,tls]',
229 'txfixtures',229 'txfixtures',
230 'txlongpoll',
231 'txlongpollfixture',
232 'txpkgupload',230 'txpkgupload',
233 'virtualenv-tools3',231 'virtualenv-tools3',
234 'wadllib',232 'wadllib',