Skip to content

Commit acb6693

Browse files
committed
feat: lab3
1 parent 96dbe9d commit acb6693

File tree

11 files changed

+187
-235
lines changed

11 files changed

+187
-235
lines changed

.github/workflows/PR.yml

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,31 @@ jobs:
1515
const { owner, repo, number: issue_number } = context.issue;
1616
const pr = await github.rest.pulls.get({ owner, repo, pull_number: issue_number });
1717
const title = pr.data.title;
18-
const labRegex = /\[LAB(\d+)\]/;
19-
const titleRegex = /^\[LAB\d+\] [\da-zA-Z]+$/;
2018
21-
if (!titleRegex.test(title)) {
22-
core.setFailed('PR title does not match the required format. Please use the format [LAB#] student#.');
19+
const titleRegex = /^\[LAB(\d+)\] [a-zA-Z]?\d+$/;
20+
const match = title.match(titleRegex);
21+
22+
let labNumberStr = undefined;
23+
if (match) {
24+
labNumberStr = match[1];
25+
} else {
26+
core.setFailed('PR title does not match the required format. Please use the format: [LAB#] <studentId>.');
2327
}
2428
25-
if (pr.data.head.ref !== pr.data.base.ref) {
26-
core.setFailed('The source branch and target branch must be the same.');
29+
const labelToAdd = `lab${labNumberStr}`;
30+
if (labNumberStr) {
31+
await github.rest.issues.addLabels({ owner, repo, issue_number, labels: [labelToAdd] });
2732
}
2833
2934
if (pr.data.base.ref === 'main') {
3035
core.setFailed('The target branch cannot be main.');
3136
}
3237
33-
const match = title.match(labRegex);
34-
if (match) {
35-
const labelToAdd = 'lab' + match[1];
36-
await github.rest.issues.addLabels({ owner, repo, issue_number, labels: [labelToAdd] });
37-
} else {
38-
core.setFailed('No match found in PR title. Please add a label in the format [LAB#] to the PR title.');
38+
if (labNumberStr < 3 && pr.data.head.ref !== pr.data.base.ref) {
39+
core.setFailed('The source branch and target branch must be the same.');
40+
}
41+
if (labNumberStr >= 3 && pr.data.head.ref !== labelToAdd) {
42+
core.setFailed(`The source branch must be '${labelToAdd}'`);
3943
}
4044
checklist-check:
4145
runs-on: ubuntu-latest
@@ -49,12 +53,12 @@ jobs:
4953
const pr = await github.rest.pulls.get({ owner, repo, pull_number: issue_number });
5054
const body = pr.data.body;
5155
52-
const checkboxes = body.match(/\- \[[x ]\]/g);
56+
const checkboxes = body.match(/^ ?(-|\*) \[[x ]\]/gmi);
5357
if (!checkboxes || checkboxes.length !== 5) {
5458
core.setFailed('The PR description must contain exactly 5 checkboxes.');
5559
}
5660
57-
const unchecked = body.match(/\- \[ \]/g);
61+
const unchecked = body.match(/^ ?(-|\*) \[ \]/gm);
5862
if (unchecked && unchecked.length > 0) {
59-
core.setFailed(`There are ${unchecked.length} unchecked items in the PR description.`);
63+
core.setFailed(`There are ${unchecked.length} unchecked item(s) in the PR description.`);
6064
}

.github/workflows/lab-autograding.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Autograding
2+
3+
on:
4+
pull_request_target:
5+
types: [labeled, synchronize, opened, reopened, ready_for_review]
6+
7+
jobs:
8+
build:
9+
runs-on: ${{ matrix.os }}
10+
strategy:
11+
matrix:
12+
os: [ubuntu-22.04]
13+
fail-fast: false
14+
steps:
15+
- uses: actions/checkout@v4
16+
with:
17+
ref: "${{ github.event.pull_request.merge_commit_sha }}"
18+
fetch-depth: 1
19+
- uses: actions/setup-node@v4
20+
with:
21+
node-version: latest
22+
- name: Extract lab number and Check no changes other than specific files
23+
uses: actions/github-script@v5
24+
id: lab
25+
with:
26+
result-encoding: string
27+
github-token: ${{ secrets.GITHUB_TOKEN }}
28+
script: |
29+
const { owner, repo, number: issue_number } = context.issue;
30+
const pr = await github.rest.pulls.get({ owner, repo, pull_number: issue_number });
31+
const labels = pr.data.labels;
32+
const lab = labels.find((label) => label.name.startsWith('lab'));
33+
if (!lab) {
34+
core.setFailed('No lab label found on the PR.');
35+
return { number: 0 };
36+
}
37+
const labNumberMatch = lab.name.match(/lab(\d+)/);
38+
if (!labNumberMatch) {
39+
core.setFailed('Invalid lab label found on the PR.');
40+
return { number: 0 };
41+
}
42+
const labNumber = labNumberMatch[1];
43+
console.log(`Lab number: ${labNumber}`)
44+
45+
const files = await github.rest.pulls.listFiles({ owner, repo, pull_number: issue_number });
46+
const changedFiles = files.data.map((file) => file.filename);
47+
const allowedFileRegex = /^lab\d+\/main_test.js$/;
48+
if (!changedFiles.every((file) => allowedFileRegex.test(file))) {
49+
core.setFailed('The PR contains changes to files other than the allowed files.');
50+
}
51+
return labNumber;
52+
- name: Grading
53+
run: |
54+
cd lab${{ steps.lab.outputs.result }}
55+
./validate.sh

.github/workflows/lab1.yml

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

.github/workflows/lab2.yml

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

lab2/main_test.js

Lines changed: 2 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -1,146 +1,6 @@
11
const test = require('node:test');
22
const assert = require('assert');
3-
const fs = require('fs');
4-
const os = require('os');
5-
const path = require('path');
6-
73
const { Application, MailSystem } = require('./main');
84

9-
test('should return a message context', () => {
10-
const mailSystem = new MailSystem();
11-
const name = 'John';
12-
13-
const context = mailSystem.write(name);
14-
assert.strictEqual(context, 'Congrats, John!');
15-
16-
});
17-
18-
test('should return true if mail is sent successfully', (t) => {
19-
const mailSystem = new MailSystem();
20-
const name = 'John';
21-
22-
// test send mail success return true
23-
t.mock.method(Math,'random', () => 1);
24-
is_send = mailSystem.send('Joy', "test mail");
25-
assert.strictEqual(is_send, true);
26-
27-
28-
// test send mail fail return false
29-
t.mock.method(Math,'random', () => 0.4);
30-
is_send = mailSystem.send('Joy', "test mail");
31-
assert.strictEqual(is_send, false);
32-
33-
});
34-
35-
36-
37-
38-
39-
test('should return name_list ', async()=>{
40-
41-
// Write mock name list to a temporary file
42-
const nameListContent = 'Alice\nBob\nCharlie';
43-
const tempFilePath = path.join('name_list.txt');
44-
fs.writeFileSync(tempFilePath, nameListContent);
45-
46-
// Attach cleanup handler to the process exit event
47-
process.on('exit', () => {
48-
if (tempFilePath) {
49-
// Clean up: Delete the temporary directory
50-
fs.unlinkSync(tempFilePath);
51-
}
52-
});
53-
54-
// Instantiate Application class and call getNames with the temporary file path
55-
const app = new Application();
56-
const [people, selected] = await app.getNames(tempFilePath);
57-
58-
// Assert the results
59-
assert.deepStrictEqual(people, ['Alice', 'Bob', 'Charlie']);
60-
assert.deepStrictEqual(selected, []);
61-
62-
});
63-
64-
65-
test('should return null if all people are selected', async (t) => {
66-
const app = new Application();
67-
app.people = ['Alice', 'Bob', 'Charlie'];
68-
app.selected = ['Alice', 'Bob', 'Charlie'];
69-
70-
const result = app.selectNextPerson();
71-
assert.strictEqual(result, null);
72-
});
73-
74-
//Test case for getRandomPerson() method
75-
test('should return a random person', () => {
76-
// Stub Math.random() to return a fixed value
77-
Math.random = () => 0.5; // Set Math.random() to always return 0.5
78-
const randomNumber = Math.random();
79-
80-
// Create an instance of the Application class
81-
const app = new Application();
82-
app.people = ['Alice', 'Bob', 'Charlie'];
83-
// Call the getRandomPerson() method
84-
const randomPerson = app.getRandomPerson();
85-
86-
// Ensure that the random person is one of the people in the list
87-
assert(app.people.includes(randomPerson));
88-
89-
});
90-
91-
test('should select and return a person who has not been selected yet', () => {
92-
const app = new Application();
93-
app.people = ['Alice', 'Bob', 'Charlie'];
94-
95-
let getRandomPersonCallCount = 0;
96-
app.getRandomPerson = () => {
97-
switch (getRandomPersonCallCount++) {
98-
case 0:
99-
return 'Alice';
100-
case 1:
101-
return 'Bob';
102-
case 2:
103-
return 'Charlie';
104-
}
105-
};
106-
107-
app.selected = ['Alice', 'Bob'];
108-
109-
const result = app.selectNextPerson();
110-
111-
assert.strictEqual(result, 'Charlie');
112-
assert.strictEqual(getRandomPersonCallCount, 3);
113-
});
114-
115-
class MockMailSystem {
116-
constructor() {
117-
this.writeCallCount = 0;
118-
this.sendCallCount = 0;
119-
}
120-
121-
write() {
122-
this.writeCallCount++;
123-
return 'Message context';
124-
}
125-
126-
send() {
127-
this.sendCallCount++;
128-
return true;
129-
}
130-
}
131-
132-
test('should call write and send methods of MailSystem for each selected person', () => {
133-
const mailSystem = new MockMailSystem();
134-
135-
const app = new Application();
136-
app.mailSystem = mailSystem;
137-
app.selected = ['Alice', 'Bob', 'Charlie'];
138-
139-
app.notifySelected();
140-
141-
assert.strictEqual(mailSystem.writeCallCount, 3);
142-
assert.strictEqual(mailSystem.sendCallCount, 3);
143-
144-
145-
});
146-
5+
// TODO: write your tests here
6+
// Remember to use Stub, Mock, and Spy when necessary

lab3/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Lab3
2+
3+
## Introduction
4+
5+
In this lab, you will write unit tests for functions implemented in `main.js`. You can learn how to use classes and functions in it by uncommenting the code in it. (But remember don't commit them on GitHub)
6+
7+
## Preparation (Important!!!)
8+
9+
1. Sync fork your branch (e.g., `SQLab:311XXXXXX`)
10+
2. `git checkout 311XXXXXX`
11+
3. `git pull`
12+
4. `git checkout -b lab3` (**NOT** your student ID !!!)
13+
14+
## Requirement
15+
16+
1. (40%) Write test cases in `main_test.js` and achieve 100% code coverage.
17+
2. (30%) For each function, parameterize its testcases to test the error-results.
18+
3. (30%) For each function, use at least 3 parameterized testcases to test the non-error-results.
19+
20+
You can run `validate.sh` in your local to test if you satisfy the requirements.
21+
22+
Please note that you must not alter files other than `main_test.js`. You will get 0 points if
23+
24+
1. you modify other files to achieve requirements.
25+
2. you can't pass all CI on your PR.
26+
27+
## Submission
28+
29+
You need to open a pull request to your branch (e.g. 311XXXXXX, your student number) and contain the code that satisfies the abovementioned requirements.
30+
31+
Moreover, please submit the URL of your PR to E3. Your submission will only be accepted when you present at both places.

lab3/main.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
class Calculator {
2+
exp(x) {
3+
if (!Number.isFinite(x)) {
4+
throw Error('unsupported operand type');
5+
}
6+
const result = Math.exp(x);
7+
if (result === Infinity) {
8+
throw Error('overflow');
9+
}
10+
return result;
11+
}
12+
13+
log(x) {
14+
if (!Number.isFinite(x)) {
15+
throw Error('unsupported operand type');
16+
}
17+
const result = Math.log(x);
18+
if (result === -Infinity) {
19+
throw Error('math domain error (1)');
20+
}
21+
if (Number.isNaN(result)) {
22+
throw Error('math domain error (2)');
23+
}
24+
return result;
25+
}
26+
}
27+
28+
// const calculator = new Calculator();
29+
// console.log(calculator.exp(87));
30+
// console.log(calculator.log(48763));
31+
32+
module.exports = {
33+
Calculator
34+
};

lab3/main_test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const { describe, it } = require('node:test');
2+
const assert = require('assert');
3+
const { Calculator } = require('./main');
4+
5+
// TODO: write your tests here

0 commit comments

Comments
 (0)