Commit c7a4e412 authored by Ludovic Le Frioux's avatar Ludovic Le Frioux
Browse files

Merge branch '14-setup-ci' into 'dev'

Resolve "Setup CI"

See merge request !20
parents 1ad842c7 bdf5158d
Pipeline #242573 passed with stages
in 0 seconds
stages:
- single-player-pipeline
- collaborative-pipeline
single-player:
stage: single-player-pipeline
trigger:
include: single-player/.gitlab-ci.yml
strategy: depend
only:
changes:
- single-player/**/*
collaborative:
stage: collaborative-pipeline
trigger:
include: collaborative/.gitlab-ci.yml
strategy: depend
only:
changes:
- collaborative/**/*
image: node:14.16.0
stages:
- build
- test
cache: &global_cache # per-branch cache
key: ${CI_COMMIT_REF_SLUG}
paths:
- collaborative/.npm/
# make push explicit
policy: pull
build:
stage: build
before_script:
- cd collaborative
script:
- npm ci --cache .npm --prefer-offline
# prepare script is skipped by install/ci when running NPM 6 as root.
# This line can be safely removed with NPM 7+ (see NPM Changelog)
- npm run prepare
# Build demo application
# Still some ESlint warnings to be fixed: disable ESlint at this stage
- DISABLE_ESLINT_PLUGIN=true npm run build
artifacts:
paths:
- collaborative/node_modules/
- collaborative/build/
expire_in: 3 days
cache:
<<: *global_cache
policy: pull-push
test:
stage: test
# node 15+ has bug #2143 : https://github.com/npm/cli/issues/2143
image: node:14.16.0
variables:
COUCHDB_HOST: couchdb
COUCHDB_PORT: 5984
COUCHDB_URL: http://$COUCHDB_HOST:$COUCHDB_PORT/
COUCHDB_USER: admin
COUCHDB_PASSWORD: "admin"
services:
- couchdb:latest
before_script:
- cd collaborative
# setup couchdb
- curl -su "$COUCHDB_USER:$COUCHDB_PASSWORD"
-X PUT $COUCHDB_URL/_users
# run c-service in background
- npx @concordant/c-service &
script:
- npm test
coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
artifacts:
paths:
- coverage/
This diff is collapsed.
......@@ -36,58 +36,14 @@ beforeEach(() => {
describe("Testing UI", () => {
/**
* This test evaluates that the cell displays the initial value.
* This test evaluates that the cell displays the value.
* Here it should display 6.
*/
test("Cell value initialization", () => {
test("Cell value", () => {
const wrapper = mount(
<Cell index={3} value={"6"} onChange={() => ({})} lock={false} />
<Cell index={3} value={"6"} onChange={() => ({})} error={false} />
);
expect(wrapper.find("textarea").text()).toBe("6");
});
/**
* This test evaluates that after updating the cell value,
* the cell display the right value.
* Here it should display 2.
*/
test("Cell value changed", () => {
const wrapper = mount(
<Cell index={3} value={"6"} onChange={() => ({})} lock={false} />
);
expect(wrapper.find("textarea").text()).toBe("6");
wrapper.find("textarea").simulate("change", { target: { value: "2" } });
expect(wrapper.find("textarea").text()).toBe("2");
});
/**
* This test evaluates that the value of a locked cell cannot be modified.
* Here it should display 2 even if we try to modify it.
*/
test("Cell locked", () => {
const wrapper = mount(
<Cell index={3} value={"6"} onChange={() => ({})} lock={true} />
);
expect(wrapper.find("textarea").text()).toBe("6");
wrapper.find("textarea").simulate("change", { target: { value: "2" } });
expect(wrapper.find("textarea").text()).toBe("6");
});
/**
* This test evaluates that the value of a cell can only be modified with integers from 1 to 9.
*/
test("Cell wrong value", () => {
const wrapper = mount(
<Cell index={3} value={"6"} onChange={() => ({})} lock={false} />
);
expect(wrapper.find("textarea").text()).toBe("6");
wrapper.find("textarea").simulate("change", { target: { value: "0" } });
expect(wrapper.find("textarea").text()).toBe("6");
wrapper.find("textarea").simulate("change", { target: { value: "10" } });
expect(wrapper.find("textarea").text()).toBe("6");
wrapper.find("textarea").simulate("change", { target: { value: "1.1" } });
expect(wrapper.find("textarea").text()).toBe("6");
wrapper.find("textarea").simulate("change", { target: { value: "abc" } });
expect(wrapper.find("textarea").text()).toBe("6");
const input = wrapper.find("input");
expect(input.instance().value).toBe("6");
});
});
......@@ -51,10 +51,6 @@ class Cell extends React.Component<ICellProps> {
}
if (event.target.value === "" || validInput.test(event.target.value)) {
this.props.onChange(this.props.index, event.target.value);
} else {
console.error(
"Invalid input in cell " + this.props.index + " : " + event.target.value
);
}
}
......
......@@ -23,43 +23,268 @@
*/
import React from "react";
import assert from "assert";
import Grid from "./Grid";
import { GRIDS } from "../constants";
import { client } from "@concordant/c-client";
import Submit1Input from "./Submit1Input";
import CONFIG from "../config.json";
const session = client.Session.Companion.connect(
CONFIG.dbName,
CONFIG.serviceUrl,
CONFIG.credentials
);
const collection = session.openCollection("sudoku", false);
/**
* Interface for the state of a Game.
* Keep a reference to the opened session and opened MVMap.
*/
interface IGameState {
session: client.Session;
collection: client.Collection;
gridNum: string;
mvmap: any; // eslint-disable-line @typescript-eslint/no-explicit-any
cells: { value: string; modifiable: boolean }[];
isConnected: boolean;
}
/**
* This class represent the Game that glues all components together.
*/
class Game extends React.Component<Record<string, unknown>, IGameState> {
timerID!: NodeJS.Timeout;
modifiedCells: string[];
constructor(props: Record<string, unknown>) {
super(props);
const session = client.Session.Companion.connect(
CONFIG.dbName,
CONFIG.serviceUrl,
CONFIG.credentials
const cells = new Array(81)
.fill(null)
.map(() => ({ value: "", modifiable: false }));
this.modifiedCells = new Array(81).fill(null);
const gridNum = "1";
const mvmap = collection.open(
"grid" + gridNum,
"MVMap",
false,
function () {
return;
}
);
const collection = session.openCollection("sudoku", false);
this.state = {
session: session,
collection: collection,
gridNum: gridNum,
mvmap: mvmap,
cells: cells,
isConnected: true,
};
}
/**
* Called after the component is rendered.
* It set a timer to refresh cells values.
*/
componentDidMount(): void {
this.initFrom(generateStaticGrid(this.state.gridNum));
this.timerID = setInterval(() => this.updateGrid(), 1000);
}
/**
* Called when the compenent is about to be removed from the DOM.
* It remove the timer set in componentDidMount().
*/
componentWillUnmount(): void {
clearInterval(this.timerID);
}
/**
* Update cells values from C-Client.
*/
updateGrid(): void {
const cells = this.state.cells;
if (!this.state.isConnected) {
console.error("updateGrid() called while not connected.");
return;
}
for (let index = 0; index < 81; index++) {
if (cells[index].modifiable) {
cells[index].value = "";
}
}
session.transaction(client.utils.ConsistencyLevel.None, () => {
const itString = this.state.mvmap.iteratorString();
while (itString.hasNext()) {
const val = itString.next();
cells[val.first].value = hashSetToString(val.second);
}
});
this.setState({ cells: cells });
}
/**
* This function is used to simulate the offline mode.
*/
switchConnection(): void {
if (this.state.isConnected) {
this.modifiedCells = new Array(81).fill(null);
clearInterval(this.timerID);
} else {
for (let index = 0; index < 81; index++) {
if (
this.state.cells[index].modifiable &&
this.modifiedCells[index] !== null
) {
session.transaction(client.utils.ConsistencyLevel.None, () => {
this.state.mvmap.setString(index, this.modifiedCells[index]);
});
}
}
this.timerID = setInterval(() => this.updateGrid(), 1000);
}
this.setState({ isConnected: !this.state.isConnected });
}
/**
* Initialize the grid with the given values.
* @param values values to be set in the grid.
*/
initFrom(values: string): void {
assert.ok(values.length === 81);
const cells = this.state.cells;
for (let index = 0; index < 81; index++) {
cells[index].value = values[index] === "." ? "" : values[index];
cells[index].modifiable = values[index] === "." ? true : false;
}
this.setState({ cells: cells });
}
/**
* Reset the value of all modifiable cells.
*/
reset(): void {
const cells = this.state.cells;
for (let index = 0; index < 81; index++) {
if (cells[index].modifiable) {
cells[index].value = "";
if (this.state.isConnected) {
session.transaction(client.utils.ConsistencyLevel.None, () => {
this.state.mvmap.setString(index, cells[index].value);
});
} else {
this.modifiedCells[index] = "";
}
}
}
this.setState({ cells: cells });
}
/**
* This handler is called when the value of a cell is changed.
* @param index The index of the cell changed.
* @param value The new value of the cell.
*/
handleChange(index: number, value: string): void {
assert.ok(value === "" || (Number(value) >= 1 && Number(value) <= 9));
assert.ok(index >= 0 && index < 81);
if (!this.state.cells[index].modifiable) {
console.error(
"Trying to change an non modifiable cell. Should not happend"
);
}
const cells = this.state.cells;
cells[index].value = value;
this.setState({ cells: cells });
if (this.state.isConnected) {
session.transaction(client.utils.ConsistencyLevel.None, () => {
this.state.mvmap.setString(index, value);
});
} else {
this.modifiedCells[index] = value;
}
}
/**
* This handler is called when a new grid number is submit.
* @param gridNum Desired grid number.
*/
handleSubmit(gridNum: string): void {
if (
Number(gridNum) < 1 ||
Number(gridNum) > 100 ||
gridNum === this.state.gridNum
) {
return;
}
const mvmap = collection.open(
"grid" + gridNum,
"MVMap",
false,
function () {
return;
}
);
this.setState({ gridNum: gridNum, mvmap: mvmap });
this.initFrom(generateStaticGrid(gridNum));
}
render(): JSX.Element {
return (
<Grid session={this.state.session} collection={this.state.collection} />
<div className="sudoku">
<div>Current grid : {this.state.gridNum}</div>
<Submit1Input
inputName="Grid"
onSubmit={this.handleSubmit.bind(this)}
/>
<div>
Difficulty levels: easy (1-20), medium (21-40), hard (41-60),
very-hard (61-80), insane (81-100)
</div>
<br />
<div>
<button onClick={this.reset.bind(this)}>Reset</button>
</div>
<br />
<div>
<button onClick={() => this.switchConnection()}>
{this.state.isConnected ? "Disconnect" : "Connect"}
</button>
</div>
<br />
<Grid
cells={this.state.cells}
onChange={(index: number, value: string) =>
this.handleChange(index, value)
}
/>
</div>
);
}
}
/**
* Return a predefined Sudoku grid as a string.
* @param gridNum Desired grid number
*/
function generateStaticGrid(gridNum: string) {
return GRIDS[gridNum];
}
/**
* Concatenates all values of a HashSet as a String.
* @param set HashSet to be concatenated.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function hashSetToString(set: any): string {
const res = new Set();
const it = set.iterator();
while (it.hasNext()) {
const val = it.next();
if (val !== "") {
res.add(val);
}
}
return Array.from(res).sort().join(" ");
}
export default Game;
This diff is collapsed.
This diff is collapsed.
image: node:14.16.0
stages:
- build
- test
cache: &global_cache # per-branch cache
key: ${CI_COMMIT_REF_SLUG}
paths:
- single-player/.npm/
# make push explicit
policy: pull
build:
stage: build
before_script:
- cd single-player
script:
- npm ci --cache .npm --prefer-offline
# prepare script is skipped by install/ci when running NPM 6 as root.
# This line can be safely removed with NPM 7+ (see NPM Changelog)
- npm run prepare
# Build demo application
# Still some ESlint warnings to be fixed: disable ESlint at this stage
- DISABLE_ESLINT_PLUGIN=true npm run build
artifacts:
paths:
- single-player/node_modules/
- single-player/build/
expire_in: 3 days
cache:
<<: *global_cache
policy: pull-push
test:
stage: test
# node 15+ has bug #2143 : https://github.com/npm/cli/issues/2143
image: node:14.16.0
variables:
COUCHDB_HOST: couchdb
COUCHDB_PORT: 5984
COUCHDB_URL: http://$COUCHDB_HOST:$COUCHDB_PORT/
COUCHDB_USER: admin
COUCHDB_PASSWORD: "admin"
services:
- couchdb:latest
before_script:
- cd single-player
# setup couchdb
- curl -su "$COUCHDB_USER:$COUCHDB_PASSWORD"
-X PUT $COUCHDB_URL/_users
# run c-service in background
- npx @concordant/c-service &
script:
- npm test
coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
artifacts:
paths:
- coverage/
......@@ -36,58 +36,14 @@ beforeEach(() => {
describe("Testing UI", () => {
/**
* This test evaluates that the cell displays the initial value.
* This test evaluates that the cell displays the value.
* Here it should display 6.
*/
test("Cell value initialization", () => {
test("Cell value", () => {
const wrapper = mount(
<Cell index={3} value={"6"} onChange={() => ({})} lock={false} />
<Cell index={3} value={"6"} onChange={() => ({})} error={false} />
);
expect(wrapper.find("textarea").text()).toBe("6");
});
/**
* This test evaluates that after updating the cell value,
* the cell display the right value.
* Here it should display 2.
*/
test("Cell value changed", () => {
const wrapper = mount(
<Cell index={3} value={"6"} onChange={() => ({})} lock={false} />
);
expect(wrapper.find("textarea").text()).toBe("6");
wrapper.find("textarea").simulate("change", { target: { value: "2" } });
expect(wrapper.find("textarea").text()).toBe("2");
});
/**
* This test evaluates that the value of a locked cell cannot be modified.
* Here it should display 2 even if we try to modify it.
*/
test("Cell locked", () => {
const wrapper = mount(
<Cell index={3} value={"6"} onChange={() => ({})} lock={true} />
);
expect(wrapper.find("textarea").text()).toBe("6");
wrapper.find("textarea").simulate("change", { target: { value: "2" } });
expect(wrapper.find("textarea").text()).toBe("6");
});
/**
* This test evaluates that the value of a cell can only be modified with integers from 1 to 9.
*/
test("Cell wrong value", () => {
const wrapper = mount(
<Cell index={3} value={"6"} onChange={() => ({})} lock={false} />
);
expect(wrapper.find("textarea").text()).toBe("6");
wrapper.find("textarea").simulate("change", { target: { value: "0" } });
expect(wrapper.find("textarea").text()).toBe("6");
wrapper.find("textarea").simulate("change", { target: { value: "10" } });
expect(wrapper.find("textarea").text()).toBe("6");
wrapper.find("textarea").simulate("change", { target: { value: "1.1" } });
expect(wrapper.find("textarea").text()).toBe("6");
wrapper.find("textarea").simulate("change", { target: { value: "abc" } });
expect(wrapper.find("textarea").text()).toBe("6");
const input = wrapper.find("input");
expect(input.instance().value).toBe("6");
});
});
......@@ -51,10 +51,6 @@ class Cell extends React.Component<ICellProps> {
}
if (event.target.value === "" || validInput.test(event.target.value)) {
this.props.onChange(this.props.index, event.target.value);
} else {
console.error(
"Invalid input in cell " + this.props.index + " : " + event.target.value