Skip to content
This repository was archived by the owner on Feb 21, 2026. It is now read-only.

Commit fafc4de

Browse files
committed
run integration testing against minio
Closes #12. * [x] Set up minio in a container on CircleCI * [x] Create a packaged release for testing * [x] Install packaged release into a virtualenv (full end-to-end integration test) * [x] Fix bug in handlers registration (this can be moved to another PR if requested) * [x] Write integration test that launches jupyter with bookstore enabled and tests that the notebook was written to disk and "s3" (minio) properly
1 parent 6f4a968 commit fafc4de

File tree

9 files changed

+330
-17
lines changed

9 files changed

+330
-17
lines changed

.circleci/config.yml

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ version: 2
66
jobs:
77
build:
88
docker:
9-
# specify the version you desire here
10-
# use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers`
11-
- image: circleci/python:3.6.1
9+
- image: circleci/python:3.6.7-node-browsers
10+
ports: 9988:9988
1211

13-
# Specify service dependencies here if necessary
14-
# CircleCI maintains a library of pre-built images
15-
# documented at https://circleci.com/docs/2.0/circleci-images/
16-
# - image: circleci/postgres:9.4
12+
- image: minio/minio:RELEASE.2018-11-06T01-01-02Z
13+
command: server /data
14+
environment:
15+
MINIO_ACCESS_KEY: ONLY_ON_CIRCLE
16+
MINIO_SECRET_KEY: CAN_WE_DO_THIS
17+
ports: 9000:9000
1718

1819
working_directory: ~/repo
1920

@@ -23,19 +24,41 @@ jobs:
2324
# Download and cache dependencies
2425
- restore_cache:
2526
keys:
26-
- v1-dependencies-{{ checksum "requirements.txt" }}
27+
- v2-dependencies-{{ checksum "requirements.txt" }}-{{ checksum "requirements-dev.txt"}}
2728
# fallback to using the latest cache if no exact match is found
28-
- v1-dependencies-
29+
- v2-dependencies-
2930

3031
- run:
3132
name: install dependencies
3233
command: |
3334
python3 -m venv venv
3435
. venv/bin/activate
36+
pip install --upgrade pip setuptools wheel
3537
pip install -r requirements.txt
3638
pip install -r requirements-dev.txt
3739
3840
- save_cache:
3941
paths:
4042
- ./venv
41-
key: v1-dependencies-{{ checksum "requirements.txt" }}-{{ checksum "requirements-dev.txt" }}
43+
key: v2-dependencies-{{ checksum "requirements.txt" }}-{{ checksum "requirements-dev.txt" }}
44+
45+
- run:
46+
name: package up bookstore
47+
command: |
48+
. venv/bin/activate
49+
# Package up the package
50+
python setup.py sdist bdist_wheel
51+
- run:
52+
name: create virtual environment for packaged release
53+
command: |
54+
python3 -m venv venv_packaged_integration
55+
. venv_packaged_integration/bin/activate
56+
pip install --upgrade pip setuptools wheel
57+
pip install -U --force-reinstall dist/bookstore*.whl
58+
- run:
59+
name: integration tests
60+
command: |
61+
. venv_packaged_integration/bin/activate
62+
# Install the dependencies for our integration tester
63+
npm i
64+
node ci/integration.js

bookstore/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
__version__ = get_versions()['version']
44
del get_versions
55

6-
from .jupyter_server_extension import load_jupyter_server_extension, _jupyter_server_extension_paths
6+
from .handlers import load_jupyter_server_extension
7+
8+
def _jupyter_server_extension_paths():
9+
return [dict(module="bookstore")]
710

811
from .archive import BookstoreContentsArchiver
912

bookstore/archive.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ def __init__(self, *args, **kwargs):
3131
# opt ourselves into being part of the Jupyter App that should have Bookstore Settings applied
3232
self.settings = BookstoreSettings(parent=self)
3333

34+
self.log.info("Archiving notebooks to {}".format(self.full_prefix))
35+
3436
self.fs = s3fs.S3FileSystem(key=self.settings.s3_access_key_id,
3537
secret=self.settings.s3_secret_access_key,
3638
client_kwargs={

bookstore/jupyter_server_extension/handlers.py renamed to bookstore/handlers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
AuthenticatedFileHandler
55
from tornado import web
66

7-
from .._version import get_versions
7+
from ._version import get_versions
88
version = get_versions()['version']
99

1010
class BookstoreVersionHandler(APIHandler):

bookstore/jupyter_server_extension/__init__.py

Lines changed: 0 additions & 4 deletions
This file was deleted.

ci/integration.js

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
const child_process = require("child_process");
2+
const { randomBytes } = require("crypto");
3+
const path = require("path");
4+
const fs = require("fs");
5+
6+
global.XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;
7+
8+
const { ajax } = require("rxjs/ajax");
9+
10+
const rxJupyter = require("rx-jupyter");
11+
12+
const _ = require("lodash");
13+
14+
console.log("running bookstore integration tests");
15+
16+
async function genToken(byteLength = 32) {
17+
return new Promise((resolve, reject) => {
18+
randomBytes(byteLength, (err, buffer) => {
19+
if (err) {
20+
reject(err);
21+
return;
22+
}
23+
24+
resolve(buffer.toString("hex"));
25+
});
26+
});
27+
}
28+
29+
const sleep = timeout =>
30+
new Promise((resolve, reject) => setTimeout(resolve, timeout));
31+
32+
// Catch all rogue promise rejections to fail CI
33+
process.on("unhandledRejection", error => {
34+
console.log("unhandledRejection", error.message);
35+
process.exit(2);
36+
});
37+
38+
var Minio = require("minio");
39+
40+
const main = async () => {
41+
const jupyterToken = await genToken();
42+
const jupyterPort = 9988;
43+
const jupyterEndpoint = `http://127.0.0.1:${jupyterPort}`;
44+
45+
const bucketName = "bookstore";
46+
// Optional according to minio docs, likely here for AWS compat
47+
const regionName = "us-east-1";
48+
49+
const s3Config = {
50+
endPoint: "127.0.0.1",
51+
port: 9000,
52+
useSSL: false,
53+
accessKey: "ONLY_ON_CIRCLE",
54+
secretKey: "CAN_WE_DO_THIS"
55+
};
56+
57+
// Instantiate the minio client with the endpoint
58+
// and access keys as shown below.
59+
var minioClient = new Minio.Client(s3Config);
60+
61+
const madeBucket = await new Promise((resolve, reject) => {
62+
minioClient.makeBucket(bucketName, regionName, err => {
63+
if (err) {
64+
reject(err);
65+
return;
66+
}
67+
resolve();
68+
});
69+
});
70+
console.log(`Created bucket ${bucketName}`);
71+
72+
const jupyter = child_process.spawn(
73+
"jupyter",
74+
[
75+
"notebook",
76+
"--no-browser",
77+
`--NotebookApp.token=${jupyterToken}`,
78+
`--NotebookApp.disable_check_xsrf=True`,
79+
`--port=${jupyterPort}`,
80+
`--ip=127.0.0.1`
81+
],
82+
{ cwd: __dirname }
83+
);
84+
85+
////// Refactor me later, streams are a bit messy with async await
86+
// Check to see that jupyter is up
87+
let jupyterUp = false;
88+
89+
jupyter.stdout.on("data", data => {
90+
const s = data.toString();
91+
console.log(s);
92+
});
93+
jupyter.stderr.on("data", data => {
94+
const s = data.toString();
95+
96+
console.error(s);
97+
if (s.includes("Jupyter Notebook is running at")) {
98+
jupyterUp = true;
99+
}
100+
});
101+
jupyter.stdout.on("end", data => console.log("DONE WITH JUPYTER"));
102+
103+
jupyter.on("exit", code => {
104+
if (code != 0) {
105+
// Jupyter exited badly
106+
console.error("jupyter errored", code);
107+
process.exit(code);
108+
}
109+
});
110+
111+
await sleep(3000);
112+
113+
if (!jupyterUp) {
114+
console.log("jupyter has not come up after 3 seconds, waiting 3 more");
115+
await sleep(3000);
116+
117+
if (!jupyterUp) {
118+
console.log("jupyter has not come up after 6 seconds, bailing");
119+
process.exit(1);
120+
}
121+
}
122+
123+
const originalNotebook = {
124+
cells: [
125+
{
126+
cell_type: "code",
127+
execution_count: null,
128+
metadata: {},
129+
outputs: [],
130+
source: ["import this"]
131+
}
132+
],
133+
metadata: {
134+
kernelspec: {
135+
display_name: "Python 3",
136+
language: "python",
137+
name: "python3"
138+
},
139+
language_info: {
140+
codemirror_mode: {
141+
name: "ipython",
142+
version: 3
143+
},
144+
file_extension: ".py",
145+
mimetype: "text/x-python",
146+
name: "python",
147+
nbconvert_exporter: "python",
148+
pygments_lexer: "ipython3",
149+
version: "3.7.0"
150+
}
151+
},
152+
nbformat: 4,
153+
nbformat_minor: 2
154+
};
155+
156+
const xhr = await ajax({
157+
url: `${jupyterEndpoint}/api/contents/ci-local-writeout.ipynb`,
158+
responseType: "json",
159+
createXHR: () => new XMLHttpRequest(),
160+
method: "PUT",
161+
body: {
162+
type: "notebook",
163+
content: originalNotebook
164+
},
165+
headers: {
166+
"Content-Type": "application/json",
167+
Authorization: `token ${jupyterToken}`
168+
}
169+
}).toPromise();
170+
171+
// Wait for minio to have the notebook
172+
// Future iterations of this script should poll to get the notebook
173+
await sleep(1000);
174+
175+
jupyter.kill();
176+
177+
//// Check the notebook we placed on S3
178+
const rawNotebook = await new Promise((resolve, reject) =>
179+
minioClient.getObject(
180+
bucketName,
181+
"ci-workspace/ci-local-writeout.ipynb",
182+
(err, dataStream) => {
183+
if (err) {
184+
console.error("wat");
185+
reject(err);
186+
return;
187+
}
188+
189+
const chunks = [];
190+
dataStream.on("data", chunk => chunks.push(chunk));
191+
dataStream.on("error", reject);
192+
dataStream.on("end", () => {
193+
resolve(Buffer.concat(chunks).toString("utf8"));
194+
});
195+
}
196+
)
197+
);
198+
199+
const notebook = JSON.parse(rawNotebook);
200+
201+
if (!_.isEqual(notebook, originalNotebook)) {
202+
console.error("original");
203+
console.error(originalNotebook);
204+
console.error("from s3");
205+
console.error(notebook);
206+
throw new Error("Notebook on S3 does not match what we sent");
207+
}
208+
209+
console.log("Notebook on S3 matches what we sent");
210+
211+
//// Check the notebook we placed on Disk
212+
const diskNotebook = await new Promise((resolve, reject) =>
213+
fs.readFile(
214+
path.join(__dirname, "ci-local-writeout.ipynb"),
215+
(err, data) => {
216+
if (err) {
217+
reject(err);
218+
} else {
219+
resolve(JSON.parse(data));
220+
}
221+
}
222+
)
223+
);
224+
225+
if (!_.isEqual(diskNotebook, originalNotebook)) {
226+
console.error("original");
227+
console.error(originalNotebook);
228+
console.error("from disk");
229+
console.error(diskNotebook);
230+
throw new Error("Notebook on Disk does not match what we sent");
231+
}
232+
233+
console.log("📚 Bookstore Integration Complete 📚");
234+
};
235+
236+
main();

ci/jupyter_notebook_config.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
4+
from bookstore import BookstoreContentsArchiver, BookstoreSettings
5+
6+
# jupyter config
7+
# At ~/.jupyter/jupyter_notebook_config.py for user installs
8+
# At __ for system installs
9+
c = get_config()
10+
11+
c.NotebookApp.contents_manager_class = BookstoreContentsArchiver
12+
13+
c.BookstoreSettings.workspace_prefix = "ci-workspace"
14+
15+
# If using minio for development
16+
c.BookstoreSettings.s3_endpoint_url = "http://127.0.0.1:9000"
17+
c.BookstoreSettings.s3_bucket = "bookstore"
18+
19+
# Straight out of `circleci/config.yml`
20+
c.BookstoreSettings.s3_access_key_id = "ONLY_ON_CIRCLE"
21+
c.BookstoreSettings.s3_secret_access_key = "CAN_WE_DO_THIS"

package.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "bookstore",
3+
"version": "1.0.0",
4+
"description": "[![Documentation Status](https://readthedocs.org/projects/bookstore/badge/?version=latest)](https://bookstore.readthedocs.io/en/latest/?badge=latest)",
5+
"main": "index.js",
6+
"directories": {
7+
"doc": "docs"
8+
},
9+
"private": "true",
10+
"scripts": {
11+
"test": "echo \"Error: no test specified\" && exit 1"
12+
},
13+
"repository": {
14+
"type": "git",
15+
"url": "git+https://github.com/nteract/bookstore.git"
16+
},
17+
"author": "Kyle Kelley <rgbkrk@gmail.com>",
18+
"license": "BSD-3-Clause",
19+
"bugs": {
20+
"url": "https://github.com/nteract/bookstore/issues"
21+
},
22+
"homepage": "https://github.com/nteract/bookstore#readme",
23+
"dependencies": {
24+
"lodash": "^4.17.11",
25+
"minio": "^7.0.1",
26+
"rx-jupyter": "^4.0.0-alpha.0",
27+
"rxjs": "^6.3.3",
28+
"spawn-rx": "^3.0.0",
29+
"xhr2": "^0.1.4",
30+
"xmlhttprequest": "^1.8.0"
31+
}
32+
}

0 commit comments

Comments
 (0)