Skip to content

Commit a1af195

Browse files
Merge pull request #350 from rstudio/kg-integration
Add jupyter intergration testing to CI
2 parents a577910 + 0d7a447 commit a1af195

18 files changed

+882
-0
lines changed

.github/workflows/main.yml

+42
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,45 @@ jobs:
172172
- name: Run tests
173173
run: |
174174
pytest -m 'vetiver'
175+
176+
test-jupyter:
177+
runs-on: ubuntu-latest
178+
env:
179+
CONNECT_LICENSE: ${{ secrets.RSC_LICENSE }}
180+
ADMIN_API_KEY: ${{ secrets.ADMIN_API_KEY }}
181+
steps:
182+
- uses: actions/checkout@v2
183+
env:
184+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
185+
- uses: extractions/setup-just@v1
186+
- name: Build Containers
187+
run: |
188+
just integration-testing/build
189+
- name: Start Connect + rsconnect-jupyter
190+
run: |
191+
just integration-testing/up
192+
193+
- name: Run Cypress Tests
194+
run: |
195+
export ADMIN_API_KEY="${{ secrets.ADMIN_API_KEY }}"
196+
just integration-testing/up-cypress
197+
198+
# Videos are captured whether the suite fails or passes
199+
- name: Save videos
200+
uses: actions/upload-artifact@v3
201+
if: success() || failure()
202+
with:
203+
name: cypress-videos
204+
path: integration-testing/cypress/videos
205+
if-no-files-found: ignore
206+
retention-days: 1
207+
208+
# Screenshots are only captured on failure
209+
- name: Save screenshots
210+
uses: actions/upload-artifact@v3
211+
if: failure()
212+
with:
213+
name: cypress-screenshots
214+
path: integration-testing/cypress/screenshots
215+
if-no-files-found: ignore
216+
retention-days: 1

integration-testing/.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/client-python/
2+
/cypress/videos/
3+
/cypress/screenshots/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Stock Report
2+
3+
## About this example
4+
5+
This stock report is generated using Python and Jupyter Notebook. Stock prices are populated from Quandl to generate a stock performance summary intended to run daily after market close.
6+
7+
8+
## Learn more
9+
10+
* [Jupyter Homepage](https://jupyter.org/)
11+
* [Jupyter Documentation](https://jupyter.org/documentation)
12+
* [Using Jupyter Notebooks in {systemDisplayName}](https://docs.rstudio.com/connect/user/jupyter-notebook/)
13+
* [User Guide for rsconnect_jupyter](https://docs.rstudio.com/rsconnect-jupyter/)
14+
15+
## Requirements
16+
17+
* Python version 3.7 or higher
18+
19+
<!-- NOTE: this file is generated -->
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
jupyter==1.0.0
2+
matplotlib>=3.0.3<=3.5.1
3+
pandas>=0.25.3,<=1.4.1

integration-testing/content/notebook/stock-report-jupyter.ipynb

+464
Large diffs are not rendered by default.
Loading

integration-testing/cypress.config.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// {
2+
// "testFiles": "**/*.spec.js",
3+
// "defaultCommandTimeout": 10000,
4+
// "responseTimeout": 60000,
5+
// "integrationFolder": "./cypress/integration/",
6+
// "viewportHeight": 800
7+
// }
8+
9+
const { defineConfig } = require('cypress')
10+
11+
module.exports = defineConfig({
12+
defaultCommandTimeout: 10000,
13+
responseTimeout: 60000,
14+
component: {
15+
viewportHeight: 800
16+
},
17+
e2e: {
18+
defaultCommandTimeout: 10000,
19+
specPattern: "cypress/e2e/**/*.cy.js",
20+
},
21+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
describe('Publishing Jupyter Notebook', () => {
2+
3+
it('Publish button loads', () => {
4+
cy.visit('http://client:9999/tree/integration-testing/content/notebook/stock-report-jupyter.ipynb');
5+
cy.get('button[data-jupyter-action="rsconnect_jupyter:publish"]').click();
6+
cy.get('a[id="publish-to-connect"]').should('be.visible')
7+
});
8+
// wait is required after every action, cypress is too fast for jupyter
9+
// https://github.com/cypress-io/cypress/issues/249
10+
it('Add Server', () => {
11+
cy.visit('http://client:9999/tree/integration-testing/content/notebook/stock-report-jupyter.ipynb');
12+
cy.wait(1000);
13+
cy.get('a[id="publish-to-connect"]').click({ force: true });
14+
cy.wait(1000);
15+
cy.get('input[id="rsc-server"]').clear().type('http://connect:3939');
16+
cy.get('input[id="rsc-api-key"]').clear().type(Cypress.env('api_key'));
17+
cy.get('input[id="rsc-servername"]').clear().type('localhost');
18+
cy.get('a[class="btn btn-primary"]').contains(' Add Server').click();
19+
cy.wait(1000);
20+
cy.get('span[class="help-block"]').should('not.have.text',"Unable to verify");
21+
});
22+
it('Publish Content', () => {
23+
cy.visit('http://client:9999/tree/integration-testing/content/notebook/stock-report-jupyter.ipynb');
24+
cy.wait(1000);
25+
cy.get('a[id="publish-to-connect"]').click({ force: true });
26+
cy.wait(1000);
27+
cy.get('button[id="rsc-add-files"]').click();
28+
cy.wait(1000);
29+
cy.get('input[name="quandl-wiki-tsla.json.gz"]').click();
30+
cy.wait(1000);
31+
cy.get('button[id="add-files-dialog-accept"]').click();
32+
cy.wait(1000);
33+
cy.get('li[class="list-group-item"]').first().should('have.text'," quandl-wiki-tsla.json.gz");
34+
cy.wait(1000);
35+
cy.get('a[class="btn btn-primary"]').last().should('have.text',"Publish").click({ force: true });
36+
cy.wait(1000);
37+
cy.get('input[name="location"]').first().click();
38+
cy.wait(1000);
39+
cy.get('a[class="btn btn-primary"]').last().should('have.text',"Next").click();
40+
cy.wait(1000);
41+
cy.get('a[class="btn btn-primary"]').last().should('have.text',"Publish").click();
42+
cy.wait(1000);
43+
// allow for 5 minutes to deploy content
44+
cy.get('span[class="fa fa-link"]', { timeout: 300000 }).last().should('have.text'," Successfully published content").click();
45+
});
46+
it('Vist Content in Connect', () => {
47+
cy.connectLogin();
48+
cy.visit('http://connect:3939');
49+
cy.get('div[class="content-table__display-name"]').first().contains('stock-report-jupyter').click();
50+
cy.contentiFrame().contains('Stock Report: TSLA');
51+
});
52+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// ***********************************************************
2+
// This example plugins/index.js can be used to load plugins
3+
//
4+
// You can change the location of this file or turn off loading
5+
// the plugins file with the 'pluginsFile' configuration option.
6+
//
7+
// You can read more here:
8+
// https://on.cypress.io/plugins-guide
9+
// ***********************************************************
10+
11+
// This function is called when a project is opened or re-opened (e.g. due to
12+
// the project's config changing)
13+
14+
module.exports = (on, config) => {
15+
// `on` is used to hook into various events Cypress emits
16+
// `config` is the resolved Cypress config
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// ***********************************************
2+
// This example commands.js shows you how to
3+
// create various custom commands and overwrite
4+
// existing commands.
5+
//
6+
// For more comprehensive examples of custom
7+
// commands please read more here:
8+
// https://on.cypress.io/custom-commands
9+
// ***********************************************
10+
//
11+
//
12+
// -- This is a parent command --
13+
// Cypress.Commands.add('login', (email, password) => { ... })
14+
//
15+
//
16+
// -- This is a child command --
17+
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
18+
//
19+
//
20+
// -- This is a dual command --
21+
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
22+
//
23+
//
24+
// -- This will overwrite an existing command --
25+
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
26+
27+
// https://www.cypress.io/blog/2020/02/12/working-with-iframes-in-cypress/
28+
Cypress.Commands.add('contentiFrame', (iframe) => {
29+
// get the iframe > document > body
30+
// and retry until the body element is not empty
31+
cy.log('contentiFrame');
32+
33+
return cy
34+
.get('iframe.appFrame', { log: false })
35+
.its('0.contentDocument.body')
36+
.should('not.be.empty')
37+
// wraps "body" DOM element to allow
38+
// chaining more Cypress commands, like ".find(...)"
39+
// https://on.cypress.io/wrap
40+
.then((body) => cy.wrap(body, { log: false }));
41+
});
42+
43+
Cypress.Commands.add('connectLogin', (user) => {
44+
cy.request('POST', 'http://connect:3939/__login__', {
45+
username: 'admin',
46+
password: 'password',
47+
})
48+
});
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// ***********************************************************
2+
// This example support/index.js is processed and
3+
// loaded automatically before your test files.
4+
//
5+
// This is a great place to put global configuration and
6+
// behavior that modifies Cypress.
7+
//
8+
// You can change the location of this file or turn off
9+
// automatically serving support files with the
10+
// 'supportFile' configuration option.
11+
//
12+
// You can read more here:
13+
// https://on.cypress.io/configuration
14+
// ***********************************************************
15+
16+
// Import commands.js using ES2015 syntax:
17+
import './commands'
18+
19+
// Alternatively you can use CommonJS syntax:
20+
// require('./commands')
+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
2+
version: '3'
3+
4+
services:
5+
client:
6+
image: client
7+
hostname: client
8+
healthcheck:
9+
test: ["CMD", "curl", "-f", "http://client:9999/tree"]
10+
interval: 40s
11+
timeout: 3s
12+
retries: 30
13+
ports:
14+
- 9999:9999
15+
build:
16+
context: ./docker
17+
dockerfile: client.Dockerfile
18+
volumes:
19+
- ../:/rsconnect-python
20+
working_dir: /rsconnect-python/integration-testing
21+
entrypoint: ''
22+
23+
connect:
24+
hostname: connect
25+
image: rstudio/rstudio-connect:latest
26+
restart: always
27+
ports:
28+
- 3939:3939
29+
volumes:
30+
- $PWD/docker/rstudio-connect.gcfg:/etc/rstudio-connect/rstudio-connect.gcfg
31+
privileged: true
32+
environment:
33+
RSTUDIO_CONNECT_HASTE: "enabled"
34+
RSC_LICENSE: ${CONNECT_LICENSE}
35+
36+
cypress:
37+
image: cypress/included:12.0.0
38+
depends_on:
39+
client:
40+
condition: service_healthy
41+
build:
42+
context: ./docker
43+
dockerfile: cypress.Dockerfile
44+
volumes:
45+
- ../:/rsconnect-python
46+
working_dir: /rsconnect-python/integration-testing/
47+
environment:
48+
ADMIN_API_KEY: ${ADMIN_API_KEY}
49+
entrypoint: ''
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
FROM python:3.9
2+
COPY ./requirements.txt .
3+
EXPOSE 9999
4+
VOLUME ../../:/rsconnect-python/
5+
6+
WORKDIR /rsconnect-python/integration-testing
7+
8+
RUN apt-get update && \
9+
apt-get -y install sudo
10+
11+
RUN mkdir -p /libs-client && \
12+
curl -fsSL https://github.com/casey/just/releases/download/1.1.2/just-1.1.2-x86_64-unknown-linux-musl.tar.gz \
13+
| tar -C /libs-client -xz just
14+
15+
ENV PATH=$PATH:/libs-client
16+
17+
RUN pip install rsconnect-jupyter && \
18+
pip install pipenv && \
19+
jupyter-nbextension install --sys-prefix --py rsconnect_jupyter
20+
21+
CMD cd ../ && \
22+
make deps dist && \
23+
pip install ./dist/rsconnect_python-*.whl && \
24+
jupyter-nbextension enable --sys-prefix --py rsconnect_jupyter && \
25+
jupyter-serverextension enable --sys-prefix --py rsconnect_jupyter && \
26+
jupyter-notebook \
27+
-y --ip='0.0.0.0' --port=9999 --no-browser --NotebookApp.token='' --allow-root
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
FROM cypress/included:12.0.0
2+
3+
RUN apt-get update -y && apt-get install -y --no-install-recommends \
4+
jq
5+
6+
RUN mkdir -p /libs-cypress && \
7+
curl -fsSL https://github.com/casey/just/releases/download/1.1.2/just-1.1.2-x86_64-unknown-linux-musl.tar.gz \
8+
| tar -C /libs-cypress -xz just
9+
10+
ENV ADMIN_API_KEY=${ADMIN_API_KEY}
11+
ENV PATH=$PATH:/libs-cypress
12+
CMD cypress run --browser chrome --env api_key=${ADMIN_API_KEY}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rsconnect-jupyter
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
; Posit Connect test environment configuration
2+
3+
[Server]
4+
SenderEmail = [email protected]
5+
EmailProvider = print
6+
DataDir = /connect-rsconnect-data
7+
EnableSitemap = true
8+
AllowConfirmedUsers = true
9+
Address = http://localhost:3939
10+
JumpStartEnabled = false
11+
12+
[HTTP]
13+
Listen = :3939
14+
NoWarning = true
15+
16+
[Python]
17+
Enabled = true
18+
Executable = /opt/python/3.8.10/bin/python
19+
Executable = /opt/python/3.9.5/bin/python
20+
21+
[Authentication]
22+
BasicAuth = true
23+
InsecureDefaultUserAPIKey = true
24+
APIKeyBcryptCost = 4
25+
26+
[Authorization]
27+
DefaultUserRole = "publisher"
28+
29+
[Database]
30+
SeedUsers = true
31+
32+
[Mount]
33+
BaseDir = /connect-rsconnect-mount

0 commit comments

Comments
 (0)