diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index ab8eabd5..b8406164 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,17 +1,17 @@ ## Description - + --- - + -- [ ] A clear title (name your pr "[LAB{lab_number}] {your_student_id}") +- [ ] A clear title (name your PR "[LAB{lab_number}] {your_student_id}") - [ ] A meaningful message for PR, as well as its commits - [ ] From your specific branch (***not main or other's branch***) merging to your branch - [ ] Excluding any irrelevant files, such as binaries, text files, or dot files -- [ ] Passing tests/CI +- [ ] Passing all CI (You should check it first to pass one of the validations in CI. However, you need to make sure your PR passes all CI after you submit it.) diff --git a/.github/workflows/PR.yml b/.github/workflows/PR.yml new file mode 100644 index 00000000..10f942ec --- /dev/null +++ b/.github/workflows/PR.yml @@ -0,0 +1,60 @@ +name: PR automation +on: + pull_request_target: + types: [opened, reopened, edited, ready_for_review] + +jobs: + labeler: + runs-on: ubuntu-latest + steps: + - name: Label PR + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.PAT }} + script: | + const { owner, repo, number: issue_number } = context.issue; + const pr = await github.rest.pulls.get({ owner, repo, pull_number: issue_number }); + const title = pr.data.title; + const labRegex = /\[LAB(\d+)\]/; + const titleRegex = /^\[LAB\d+\] [\da-zA-Z]+$/; + + if (!titleRegex.test(title)) { + core.setFailed('PR title does not match the required format. Please use the format [LAB#] student#.'); + } + + if (pr.data.head.ref !== pr.data.base.ref) { + core.setFailed('The source branch and target branch must be the same.'); + } + + if (pr.data.base.ref === 'main') { + core.setFailed('The target branch cannot be main.'); + } + + const match = title.match(labRegex); + if (match) { + const labelToAdd = 'lab' + match[1]; + await github.rest.issues.addLabels({ owner, repo, issue_number, labels: [labelToAdd] }); + } else { + core.setFailed('No match found in PR title. Please add a label in the format [LAB#] to the PR title.'); + } + checklist-check: + runs-on: ubuntu-latest + steps: + - name: Check PR description + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { owner, repo, number: issue_number } = context.issue; + const pr = await github.rest.pulls.get({ owner, repo, pull_number: issue_number }); + const body = pr.data.body; + + const checkboxes = body.match(/\- \[[x ]\]/g); + if (!checkboxes || checkboxes.length !== 5) { + core.setFailed('The PR description must contain exactly 5 checkboxes.'); + } + + const unchecked = body.match(/\- \[ \]/g); + if (unchecked && unchecked.length > 0) { + core.setFailed(`There are ${unchecked.length} unchecked items in the PR description.`); + } diff --git a/.github/workflows/lab1.yml b/.github/workflows/lab1.yml index 62e939c5..49a5d578 100644 --- a/.github/workflows/lab1.yml +++ b/.github/workflows/lab1.yml @@ -1,12 +1,12 @@ -name: lab1 +name: lab1 autograding on: pull_request: - types: [labeled] + types: [labeled, synchronize, opened, reopened, ready_for_review] jobs: build: - if: ${{ github.event.label.name == 'lab1' }} + if: contains(github.event.pull_request.labels.*.name, 'lab1') runs-on: ${{ matrix.os }} strategy: matrix: @@ -22,6 +22,5 @@ jobs: sudo apt-get install -y nodejs - name: grading run: | - echo "cd lab1" cd lab1 ./validate.sh diff --git a/.github/workflows/lab2.yml b/.github/workflows/lab2.yml new file mode 100644 index 00000000..2cb76fa7 --- /dev/null +++ b/.github/workflows/lab2.yml @@ -0,0 +1,26 @@ +name: lab2 autograding + +on: + pull_request: + types: [labeled, synchronize, opened, reopened, ready_for_review] + +jobs: + build: + if: contains(github.event.pull_request.labels.*.name, 'lab2') + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-22.04] + fail-fast: false + steps: + - uses: actions/checkout@v1 + with: + fetch-depth: 1 + - name: dependency (ubuntu) + run: | + curl -fsSL https://deb.nodesource.com/setup_21.x | sudo -E bash - &&\ + sudo apt-get install -y nodejs + - name: grading + run: | + cd lab2 + ./validate.sh diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml deleted file mode 100644 index 4e7902a5..00000000 --- a/.github/workflows/label.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Automatic PR Labeler -on: - pull_request_target: - types: [opened, reopened, edited] - -jobs: - labeler: - runs-on: ubuntu-latest - steps: - - name: Label PR - uses: actions/github-script@v5 - with: - github-token: ${{ secrets.PAT }} - script: | - const { owner, repo, number: issue_number } = context.issue; - const pr = await github.rest.pulls.get({ owner, repo, pull_number: issue_number }); - const title = pr.data.title; - const labRegex = /\[LAB(\d+)\]/i; - - const match = title.match(labRegex); - if (match) { - const labelToAdd = 'lab' + match[1]; - await github.rest.issues.addLabels({ owner, repo, issue_number, labels: [labelToAdd] }); - } diff --git a/lab1/README.md b/lab1/README.md index 779166ca..78ecd97a 100644 --- a/lab1/README.md +++ b/lab1/README.md @@ -2,7 +2,7 @@ ## Introduction -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 `main.js.` +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. ## Requirement diff --git a/lab2/README.md b/lab2/README.md new file mode 100644 index 00000000..60a9c805 --- /dev/null +++ b/lab2/README.md @@ -0,0 +1,22 @@ +# Lab2 + +## Introduction + +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) + +## Requirement + +1. Write test cases in `main_test.js` and achieve 100% code coverage. Remember to use Mock, Spy, or Stub when necessary, you need to at least use one of them in your test cases. (100%) + +You can run `validate.sh` in your local to test if you satisfy the requirements. + +Please note that you must not alter files other than `main_test.js`. You will get 0 points if + +1. you modify other files to achieve requirements. +2. you can't pass all CI on your PR. + +## Submission + +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. + +Moreover, please submit the URL of your PR to E3. Your submission will only be accepted when you present at both places. diff --git a/lab2/main.js b/lab2/main.js new file mode 100644 index 00000000..2e159e75 --- /dev/null +++ b/lab2/main.js @@ -0,0 +1,81 @@ +const fs = require('fs'); +const util = require('util'); +const readFile = util.promisify(fs.readFile); + +class MailSystem { + write(name) { + console.log('--write mail for ' + name + '--'); + const context = 'Congrats, ' + name + '!'; + return context; + } + + send(name, context) { + console.log('--send mail to ' + name + '--'); + // Interact with mail system and send mail + // random success or failure + const success = Math.random() > 0.5; + if (success) { + console.log('mail sent'); + } else { + console.log('mail failed'); + } + return success; + } +} + +class Application { + constructor() { + this.people = []; + this.selected = []; + this.mailSystem = new MailSystem(); + this.getNames().then(([people, selected]) => { + this.people = people; + this.selected = selected; + }); + } + + async getNames() { + const data = await readFile('name_list.txt', 'utf8'); + const people = data.split('\n'); + const selected = []; + return [people, selected]; + } + + getRandomPerson() { + const i = Math.floor(Math.random() * this.people.length); + return this.people[i]; + } + + selectNextPerson() { + console.log('--select next person--'); + if (this.people.length === this.selected.length) { + console.log('all selected'); + return null; + } + let person = this.getRandomPerson(); + while (this.selected.includes(person)) { + person = this.getRandomPerson(); + } + this.selected.push(person); + return person; + } + + notifySelected() { + console.log('--notify selected--'); + for (const x of this.selected) { + const context = this.mailSystem.write(x); + this.mailSystem.send(x, context); + } + } +} + +// const app = new Application(); +// app.selectNextPerson(); +// app.selectNextPerson(); +// app.selectNextPerson(); +// app.notifySelected(); + +module.exports = { + Application, + MailSystem, +}; \ No newline at end of file diff --git a/lab2/main_test.js b/lab2/main_test.js new file mode 100644 index 00000000..5034468e --- /dev/null +++ b/lab2/main_test.js @@ -0,0 +1,6 @@ +const test = require('node:test'); +const assert = require('assert'); +const { Application, MailSystem } = require('./main'); + +// TODO: write your tests here +// Remember to use Stub, Mock, and Spy when necessary \ No newline at end of file diff --git a/lab2/validate.sh b/lab2/validate.sh new file mode 100755 index 00000000..13b53ed8 --- /dev/null +++ b/lab2/validate.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Check for unwanted files +for file in *; do + if [[ $file != "main.js" && $file != "main_test.js" && $file != "README.md" && $file != "validate.sh" ]]; then + echo "[!] Unwanted file detected: $file." + exit 1 + fi +done + +node=$(which node) +test_path="${BASH_SOURCE[0]}" +solution_path="$(realpath .)" +tmp_dir=$(mktemp -d -t lab2-XXXXXXXXXX) + +cd $tmp_dir + +rm -rf * +cp $solution_path/*.js . +result=$($"node" --test --experimental-test-coverage) ; ret=$? +if [ $ret -ne 0 ] ; then + echo "[!] testing fails" + exit 1 +else + coverage=$(echo "$result" | grep 'all files' | awk -F '|' '{print $2}' | sed 's/ //g') + if (( $(echo "$coverage < 100" | bc -l) )); then + echo "[!] Coverage is only $coverage%" + exit 1 + else + echo "[V] Coverage is 100%" + fi +fi + +rm -rf $tmp_dir + +exit 0 + +# vim: set fenc=utf8 ff=unix et sw=2 ts=2 sts=2: \ No newline at end of file diff --git a/scripts/rebase-all.sh b/scripts/rebase-all.sh index 26eef16b..6e104473 100755 --- a/scripts/rebase-all.sh +++ b/scripts/rebase-all.sh @@ -12,18 +12,12 @@ for branch in $(git branch -r | grep -v HEAD); do echo "Checkout failed for branch $branch" exit 1 fi - git pull origin "$branch" - if [[ $? -ne 0 ]]; then - echo "Pull failed for branch $branch" - exit 1 - fi git rebase main if [[ $? -ne 0 ]]; then echo "Rebase failed for branch $branch" exit 1 fi - git push origin "$branch" fi done -git checkout main \ No newline at end of file +git checkout main