feat: add Arduino lesson support, documentation, and load testing utilities with improved CLI feedback
parent
314975ac65
commit
10548b1f51
|
|
@ -1 +1,4 @@
|
|||
__pycache__
|
||||
test_data.json
|
||||
load-test/env
|
||||
load-test/test_data.json
|
||||
116
README.md
116
README.md
|
|
@ -174,6 +174,88 @@ Untuk materi yang melibatkan simulator rangkaian elektronika:
|
|||
| `---EXPECTED_CIRCUIT_OUTPUT---` | Validasi rangkaian (format JSON) |
|
||||
| `---KEY_TEXT_CIRCUIT---` | Kata kunci rangkaian |
|
||||
|
||||
#### Blok Khusus Arduino/Velxio
|
||||
|
||||
Untuk materi yang menggunakan simulator Arduino (Velxio):
|
||||
|
||||
| Blok | Fungsi |
|
||||
|------|--------|
|
||||
| `---INITIAL_CODE_ARDUINO---` | Kode Arduino awal di editor Velxio |
|
||||
| `---VELXIO_CIRCUIT---` | Rangkaian komponen (JSON: board, components, wires) |
|
||||
| `---EXPECTED_SERIAL_OUTPUT---` | Output serial yang diharapkan (subsequence match) |
|
||||
| `---EXPECTED_WIRING---` | Wiring yang harus dibuat siswa (JSON, lenient) |
|
||||
| `---KEY_TEXT---` | Kata kunci yang harus ada di kode siswa |
|
||||
|
||||
Contoh materi Arduino:
|
||||
|
||||
```markdown
|
||||
---INITIAL_CODE_ARDUINO---
|
||||
void setup() {
|
||||
pinMode(13, OUTPUT);
|
||||
Serial.begin(9600);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
digitalWrite(13, HIGH);
|
||||
Serial.println("LED ON");
|
||||
delay(1000);
|
||||
digitalWrite(13, LOW);
|
||||
Serial.println("LED OFF");
|
||||
delay(1000);
|
||||
}
|
||||
---END_INITIAL_CODE_ARDUINO---
|
||||
|
||||
---VELXIO_CIRCUIT---
|
||||
{
|
||||
"board": "arduino:avr:uno",
|
||||
"components": [
|
||||
{ "type": "wokwi-led", "id": "led-1", "x": 400, "y": -200, "props": { "color": "red", "pin": 13 } }
|
||||
],
|
||||
"wires": []
|
||||
}
|
||||
---END_VELXIO_CIRCUIT---
|
||||
|
||||
---EXPECTED_SERIAL_OUTPUT---
|
||||
LED ON
|
||||
LED OFF
|
||||
---END_EXPECTED_SERIAL_OUTPUT---
|
||||
|
||||
---EXPECTED_WIRING---
|
||||
{
|
||||
"wires": [
|
||||
{ "start": { "componentId": "arduino-uno", "pinName": "13" }, "end": { "componentId": "led-1", "pinName": "A" } },
|
||||
{ "start": { "componentId": "led-1", "pinName": "C" }, "end": { "componentId": "arduino-uno", "pinName": "GND" } }
|
||||
]
|
||||
}
|
||||
---END_EXPECTED_WIRING---
|
||||
|
||||
---KEY_TEXT---
|
||||
pinMode
|
||||
digitalWrite
|
||||
---END_KEY_TEXT---
|
||||
```
|
||||
|
||||
##### Referensi Nama Pin Komponen Velxio
|
||||
|
||||
| Komponen | Tipe Wokwi | Pin Names |
|
||||
|----------|------------|----------|
|
||||
| Arduino Uno | `wokwi-arduino-uno` | `0`-`13`, `A0`-`A5`, `GND`, `5V`, `3.3V` |
|
||||
| LED | `wokwi-led` | `A` (Anode), `C` (Cathode) |
|
||||
| Push Button | `wokwi-pushbutton` | `1.l`, `2.l`, `1.r`, `2.r` |
|
||||
| Resistor | `wokwi-resistor` | `1`, `2` |
|
||||
| RGB LED | `wokwi-rgb-led` | `R`, `G`, `B`, `COM` |
|
||||
|
||||
> **Penting:** Nama pin harus **persis** seperti tabel di atas.
|
||||
> `componentId` untuk Arduino Uno selalu `arduino-uno` (huruf kecil, dengan strip).
|
||||
|
||||
##### Evaluasi Arduino
|
||||
|
||||
Sistem mengevaluasi 3 aspek (semua harus lulus):
|
||||
|
||||
1. **Key Text** — kata kunci wajib ada di source code siswa
|
||||
2. **Serial Output** — baris yang diharapkan harus muncul dalam urutan (subsequence match)
|
||||
3. **Wiring** — koneksi yang diharapkan harus ada (lenient: extra wires OK, GND.1/GND.2 dinormalisasi)
|
||||
|
||||
### 5. Generate Token Siswa
|
||||
|
||||
Setelah materi siap, generate file `tokens_siswa.csv`:
|
||||
|
|
@ -244,15 +326,25 @@ Folder `examples/` berisi contoh lengkap yang digunakan oleh `./elemes.sh init`:
|
|||
```
|
||||
examples/
|
||||
├── content/
|
||||
│ ├── home.md # Halaman utama (3 materi)
|
||||
│ ├── home.md # Halaman utama (7 materi)
|
||||
│ ├── hello_world.md # Materi dasar: Hello World
|
||||
│ ├── variabel.md # Materi dasar: Variabel
|
||||
│ └── rangkaian_dasar.md # Materi hybrid: C + Circuit
|
||||
│ ├── rangkaian_dasar.md # Materi hybrid: C + Circuit
|
||||
│ ├── led_blink_arduino.md # Arduino: LED Blink + wiring
|
||||
│ ├── hello_serial_arduino.md # Arduino: Serial Monitor (tanpa wiring)
|
||||
│ ├── button_input_arduino.md # Arduino: Button + LED input/output
|
||||
│ └── traffic_light_arduino.md # Arduino: Lampu lalu lintas 3 LED
|
||||
└── tokens_siswa.csv # Data siswa contoh (1 guru + 3 siswa)
|
||||
```
|
||||
|
||||
File `rangkaian_dasar.md` adalah contoh **materi hybrid** yang menggabungkan
|
||||
latihan pemrograman C dan simulator rangkaian elektronika dalam satu lesson.
|
||||
### Jenis Materi
|
||||
|
||||
| Tipe | Contoh | Evaluasi |
|
||||
|------|--------|----------|
|
||||
| **C/Python** | `hello_world.md`, `variabel.md` | Output stdout |
|
||||
| **Hybrid** | `rangkaian_dasar.md` | C output + node voltage |
|
||||
| **Arduino (Velxio)** | `led_blink_arduino.md` | Key text + serial + wiring |
|
||||
| **Arduino (tanpa wiring)** | `hello_serial_arduino.md` | Key text + serial |
|
||||
|
||||
## FAQ
|
||||
|
||||
|
|
@ -276,6 +368,22 @@ akan otomatis muncul.
|
|||
Tidak. Tailscale opsional untuk akses remote. Tanpa Tailscale, LMS
|
||||
bisa diakses di jaringan lokal via `http://localhost:3000`.
|
||||
|
||||
**Q: Bagaimana membuat materi Arduino baru?**
|
||||
Buat file `.md` baru di `content/` dengan blok `---INITIAL_CODE_ARDUINO---`
|
||||
dan `---VELXIO_CIRCUIT---`. Lihat contoh di `led_blink_arduino.md`
|
||||
atau `button_input_arduino.md`. Pastikan `componentId` dan `pinName`
|
||||
di `EXPECTED_WIRING` sesuai dengan tabel referensi pin di atas.
|
||||
|
||||
**Q: Materi Arduino bisa tanpa wiring?**
|
||||
Ya. Jika hanya ingin evaluasi kode + serial output, cukup sertakan
|
||||
`VELXIO_CIRCUIT` dengan `components: []` kosong dan hilangkan blok
|
||||
`EXPECTED_WIRING`. Contoh: `hello_serial_arduino.md`.
|
||||
|
||||
**Q: Apakah wiring harus persis sama?**
|
||||
Tidak harus. Evaluasi bersifat *lenient*: koneksi yang diharapkan harus ada,
|
||||
tapi siswa boleh menambahkan kabel ekstra. Pin GND juga dinormalisasi
|
||||
(GND.1 = GND.2 = GND).
|
||||
|
||||
## Persyaratan Sistem
|
||||
|
||||
- [Podman](https://podman.io/) dan `podman-compose`
|
||||
|
|
|
|||
79
elemes.sh
79
elemes.sh
|
|
@ -7,24 +7,24 @@ PROJECT_NAME="$(basename "$PARENT_DIR")"
|
|||
|
||||
case "$1" in
|
||||
init)
|
||||
echo "=== Elemes Quick Start ==="
|
||||
echo "✨ === Elemes Quick Start === ✨"
|
||||
echo ""
|
||||
|
||||
# .env
|
||||
if [ -f "$PARENT_DIR/.env" ]; then
|
||||
echo "[skip] .env sudah ada"
|
||||
echo "✅ [Skip] .env sudah ada"
|
||||
else
|
||||
cp "$EXAMPLES_DIR/../.env.example" "$PARENT_DIR/.env"
|
||||
echo "[buat] .env (edit sesuai kebutuhan)"
|
||||
echo "📝 [Buat] .env (pastikan untuk edit sesuai kebutuhanmu)"
|
||||
fi
|
||||
|
||||
# content/
|
||||
if [ -d "$PARENT_DIR/content" ] && [ "$(ls -A "$PARENT_DIR/content" 2>/dev/null)" ]; then
|
||||
echo "[skip] content/ sudah ada"
|
||||
echo "✅ [Skip] Folder content/ sudah ada"
|
||||
else
|
||||
mkdir -p "$PARENT_DIR/content"
|
||||
cp -n "$EXAMPLES_DIR/content/"*.md "$PARENT_DIR/content/"
|
||||
echo "[buat] content/ ($(ls "$PARENT_DIR/content/"*.md 2>/dev/null | wc -l) materi)"
|
||||
echo "📁 [Buat] Folder content/ ($(ls "$PARENT_DIR/content/"*.md 2>/dev/null | wc -l) materi contoh ditambahkan)"
|
||||
fi
|
||||
|
||||
# assets/
|
||||
|
|
@ -32,47 +32,84 @@ init)
|
|||
|
||||
# tokens
|
||||
if [ -f "$PARENT_DIR/tokens_siswa.csv" ]; then
|
||||
echo "[skip] tokens_siswa.csv sudah ada"
|
||||
echo "✅ [Skip] tokens_siswa.csv sudah ada"
|
||||
else
|
||||
cp "$EXAMPLES_DIR/tokens_siswa.csv" "$PARENT_DIR/tokens_siswa.csv"
|
||||
echo "[buat] tokens_siswa.csv (edit untuk tambah siswa)"
|
||||
echo "👥 [Buat] tokens_siswa.csv (edit untuk menambah data siswa asli)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Selesai! Langkah selanjutnya:"
|
||||
echo " 1. Edit ../.env sesuai kebutuhan"
|
||||
echo " 2. Edit ../content/home.md untuk daftar materi"
|
||||
echo " 3. Edit ../tokens_siswa.csv untuk data siswa"
|
||||
echo " 4. Jalankan: ./elemes.sh runbuild"
|
||||
echo "🎯 Selesai! Langkah selanjutnya yang direkomendasikan:"
|
||||
echo " 👉 1. Edit file ../.env sesuai dengan kebutuhan environment-mu"
|
||||
echo " 👉 2. Edit ../content/home.md untuk menyusun daftar materi"
|
||||
echo " 👉 3. Edit ../tokens_siswa.csv untuk mendaftarkan akun siswa"
|
||||
echo " 🚀 4. Jalankan: ./elemes.sh runbuild"
|
||||
echo ""
|
||||
;;
|
||||
stop | run | runbuild | runclearbuild)
|
||||
echo "Stop Container..."
|
||||
echo "🛑 Menghentikan container yang sedang berjalan..."
|
||||
podman-compose -p "$PROJECT_NAME" --env-file ../.env down
|
||||
;;&
|
||||
stop) ;;
|
||||
stop)
|
||||
echo "✅ Container berhasil dihentikan."
|
||||
;;
|
||||
runclearbuild)
|
||||
echo "Cleanup dangling images..."
|
||||
echo "🧹 Membersihkan container dan image (prune)..."
|
||||
podman image prune -f
|
||||
echo "Build Container (no cache)..."
|
||||
echo "🏗️ Membangun ulang container dari awal (no-cache)..."
|
||||
podman-compose -p "$PROJECT_NAME" --env-file ../.env build --no-cache
|
||||
;;&
|
||||
runbuild)
|
||||
echo "Build Container..."
|
||||
echo "🏗️ Membangun container..."
|
||||
podman-compose -p "$PROJECT_NAME" --env-file ../.env build
|
||||
;;&
|
||||
runbuild | runclearbuild)
|
||||
echo "Run Container..."
|
||||
echo "🚀 Menjalankan container di background..."
|
||||
podman-compose -p "$PROJECT_NAME" --env-file ../.env up --force-recreate -d
|
||||
echo "✅ Elemes berhasil dijalankan!"
|
||||
;;
|
||||
run)
|
||||
echo "Run Container..."
|
||||
echo "🚀 Menjalankan container..."
|
||||
podman-compose -p "$PROJECT_NAME" --env-file ../.env up -d
|
||||
echo "✅ Elemes berhasil dijalankan!"
|
||||
;;
|
||||
generatetoken)
|
||||
echo "Generating tokens_siswa.csv from content..."
|
||||
echo "🔄 Melakukan sinkronisasi tokens_siswa.csv dari materi yang ada..."
|
||||
python3 "$SCRIPT_DIR/generate_tokens.py"
|
||||
;;
|
||||
loadtest)
|
||||
echo "📊 === Elemes Load Testing ==="
|
||||
cd "$SCRIPT_DIR/load-test" || exit
|
||||
|
||||
if [ ! -d "env" ]; then
|
||||
echo "⚙️ Membuat Python Virtual Environment (env/)..."
|
||||
python3 -m venv env
|
||||
fi
|
||||
|
||||
echo "⚙️ Mengaktifkan environment & menginstall requirements..."
|
||||
source env/bin/activate
|
||||
pip install -r requirements.txt > /dev/null 2>&1
|
||||
|
||||
echo "⚙️ Mempersiapkan Test Data & menginjeksi akun Bot..."
|
||||
python3 content_parser.py --num-tokens 50
|
||||
|
||||
echo ""
|
||||
echo "🚀 Memulai Locust Test Suite..."
|
||||
echo "👉 Buka http://localhost:8089 di browser web milikmu."
|
||||
echo "👉 Masukkan URL backend Elemes sebagai Host (contoh: http://localhost:5000)"
|
||||
echo "👉 Tekan CTRL+C di terminal ini untuk menghentikan test."
|
||||
echo ""
|
||||
|
||||
locust -f locustfile.py
|
||||
;;
|
||||
*)
|
||||
echo "elemes.sh ( init | run | runbuild | runclearbuild | stop | generatetoken )"
|
||||
echo "💡 Cara Penggunaan elemes.sh:"
|
||||
echo " ./elemes.sh init # Inisialisasi konfigurasi, folder, & data tokens"
|
||||
echo " ./elemes.sh run # Menjalankan container LMS yang sudah ada"
|
||||
echo " ./elemes.sh runbuild # Build image lalu jalankan container"
|
||||
echo " ./elemes.sh runclearbuild # Bersihkan cache, Re-build total, lalu jalankan"
|
||||
echo " ./elemes.sh stop # Menghentikan container yang sedang berjalan"
|
||||
echo " ./elemes.sh generatetoken # Sinkronisasi kolom tokens CSV sesuai file markdown"
|
||||
echo " ./elemes.sh loadtest # Menjalankan utilitas simulasi Load Test (Locust)"
|
||||
;;
|
||||
esac
|
||||
|
|
|
|||
|
|
@ -0,0 +1,206 @@
|
|||
---LESSON_INFO---
|
||||
Pelajaran Arduino: Membaca input tombol dan mengontrol LED.
|
||||
|
||||
**Learning Objectives:**
|
||||
- Memahami fungsi `digitalRead()` untuk membaca input digital
|
||||
- Menggunakan `INPUT_PULLUP` untuk push button
|
||||
- Menerapkan logika kondisional (`if-else`) berdasarkan input
|
||||
- Menghubungkan push button dan LED ke Arduino
|
||||
|
||||
**Prerequisites:**
|
||||
- Hello, World!
|
||||
- LED Blink
|
||||
- Hello Serial
|
||||
---END_LESSON_INFO---
|
||||
|
||||
# Button Input — Membaca Tombol
|
||||
|
||||
Setelah belajar **output** (menyalakan LED) dan **serial** (mengirim teks),
|
||||
sekarang kita belajar **input** — membaca tombol yang ditekan oleh pengguna.
|
||||
|
||||
## Konsep Digital Input
|
||||
|
||||
Setiap pin digital Arduino bisa dikonfigurasi sebagai **INPUT** atau **OUTPUT**:
|
||||
|
||||
```
|
||||
pinMode(13, OUTPUT); // Pin 13 = output (LED)
|
||||
pinMode(2, INPUT_PULLUP); // Pin 2 = input dengan pullup
|
||||
```
|
||||
|
||||
### Apa itu INPUT_PULLUP?
|
||||
|
||||
Tanpa resistor, pin input Arduino bisa membaca nilai **acak** (mengambang).
|
||||
`INPUT_PULLUP` mengaktifkan resistor internal Arduino yang menarik pin ke **HIGH** (5V).
|
||||
|
||||
```
|
||||
Tanpa pullup: Pin ──?── (nilai acak: HIGH atau LOW)
|
||||
Dengan pullup: Pin ──R──► 5V (default HIGH, jadi LOW saat ditekan)
|
||||
```
|
||||
|
||||
Saat tombol **tidak ditekan**: `digitalRead()` = `HIGH` (1)
|
||||
Saat tombol **ditekan**: `digitalRead()` = `LOW` (0)
|
||||
|
||||
> **Perhatikan:** Logikanya terbalik! Tombol ditekan = LOW, tidak ditekan = HIGH.
|
||||
|
||||
## Fungsi digitalRead()
|
||||
|
||||
```
|
||||
int nilai = digitalRead(2); // Baca pin 2
|
||||
if (nilai == LOW) {
|
||||
// Tombol sedang ditekan
|
||||
}
|
||||
```
|
||||
|
||||
## Rangkaian
|
||||
|
||||
Hubungkan push button dan LED ke Arduino:
|
||||
|
||||
```
|
||||
Pin 2 ──► Button pin 1.l (input)
|
||||
Button pin 2.l ──► GND (ground)
|
||||
|
||||
Pin 13 ──► LED Anode (A)
|
||||
LED Cathode (C) ──► GND
|
||||
```
|
||||
|
||||
Push button menggunakan `INPUT_PULLUP`, jadi hanya perlu 2 kabel: dari pin Arduino ke tombol, dan dari tombol ke GND. Tidak perlu resistor eksternal!
|
||||
|
||||
## Contoh Program
|
||||
|
||||
```
|
||||
const int BUTTON_PIN = 2;
|
||||
const int LED_PIN = 13;
|
||||
|
||||
void setup() {
|
||||
pinMode(BUTTON_PIN, INPUT_PULLUP);
|
||||
pinMode(LED_PIN, OUTPUT);
|
||||
Serial.begin(9600);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
int state = digitalRead(BUTTON_PIN);
|
||||
|
||||
if (state == LOW) {
|
||||
digitalWrite(LED_PIN, HIGH);
|
||||
Serial.println("PRESSED");
|
||||
} else {
|
||||
digitalWrite(LED_PIN, LOW);
|
||||
Serial.println("RELEASED");
|
||||
}
|
||||
|
||||
delay(100);
|
||||
}
|
||||
```
|
||||
|
||||
---EXERCISE---
|
||||
### Tantangan
|
||||
|
||||
**Kode Arduino:**
|
||||
Tulis program yang membaca tombol di **pin 2** dan mengontrol LED di **pin 13**:
|
||||
- Saat tombol ditekan → LED menyala, cetak `PRESSED` ke Serial Monitor
|
||||
- Saat tombol dilepas → LED mati, cetak `RELEASED` ke Serial Monitor
|
||||
|
||||
Gunakan `INPUT_PULLUP` agar tidak perlu resistor tambahan untuk tombol.
|
||||
|
||||
**Rangkaian:**
|
||||
Hubungkan komponen berikut di simulator:
|
||||
- Pin 2 Arduino → Button (pin 1.l)
|
||||
- Button (pin 2.l) → GND Arduino
|
||||
- Pin 13 Arduino → LED Anode (A)
|
||||
- LED Cathode (C) → GND Arduino
|
||||
|
||||
Setelah selesai, tekan **Compile & Run**, lalu **klik tombol** di simulator untuk menguji.
|
||||
---
|
||||
|
||||
---INITIAL_CODE_ARDUINO---
|
||||
// Button Input - Membaca Tombol
|
||||
// Tekan tombol untuk menyalakan LED
|
||||
|
||||
const int BUTTON_PIN = 2;
|
||||
const int LED_PIN = 13;
|
||||
|
||||
void setup() {
|
||||
// Setup pin mode
|
||||
pinMode(BUTTON_PIN, INPUT_PULLUP);
|
||||
pinMode(LED_PIN, OUTPUT);
|
||||
Serial.begin(9600);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
int buttonState = digitalRead(BUTTON_PIN);
|
||||
|
||||
if (buttonState == LOW) {
|
||||
// Tombol ditekan
|
||||
digitalWrite(LED_PIN, HIGH);
|
||||
Serial.println("PRESSED");
|
||||
} else {
|
||||
// Tombol dilepas
|
||||
digitalWrite(LED_PIN, LOW);
|
||||
Serial.println("RELEASED");
|
||||
}
|
||||
|
||||
delay(100);
|
||||
}
|
||||
---END_INITIAL_CODE_ARDUINO---
|
||||
|
||||
---VELXIO_CIRCUIT---
|
||||
{
|
||||
"board": "arduino:avr:uno",
|
||||
"components": [
|
||||
{
|
||||
"type": "wokwi-pushbutton",
|
||||
"id": "button-1",
|
||||
"x": 400,
|
||||
"y": -100,
|
||||
"rotation": 0,
|
||||
"props": {}
|
||||
},
|
||||
{
|
||||
"type": "wokwi-led",
|
||||
"id": "led-1",
|
||||
"x": 400,
|
||||
"y": -250,
|
||||
"rotation": 0,
|
||||
"props": {
|
||||
"color": "green",
|
||||
"pin": 13
|
||||
}
|
||||
}
|
||||
],
|
||||
"wires": []
|
||||
}
|
||||
---END_VELXIO_CIRCUIT---
|
||||
|
||||
---EXPECTED_SERIAL_OUTPUT---
|
||||
PRESSED
|
||||
---END_EXPECTED_SERIAL_OUTPUT---
|
||||
|
||||
---EXPECTED_WIRING---
|
||||
{
|
||||
"wires": [
|
||||
{
|
||||
"start": { "componentId": "arduino-uno", "pinName": "2" },
|
||||
"end": { "componentId": "button-1", "pinName": "1.l" }
|
||||
},
|
||||
{
|
||||
"start": { "componentId": "button-1", "pinName": "2.l" },
|
||||
"end": { "componentId": "arduino-uno", "pinName": "GND" }
|
||||
},
|
||||
{
|
||||
"start": { "componentId": "arduino-uno", "pinName": "13" },
|
||||
"end": { "componentId": "led-1", "pinName": "A" }
|
||||
},
|
||||
{
|
||||
"start": { "componentId": "led-1", "pinName": "C" },
|
||||
"end": { "componentId": "arduino-uno", "pinName": "GND" }
|
||||
}
|
||||
]
|
||||
}
|
||||
---END_EXPECTED_WIRING---
|
||||
|
||||
---KEY_TEXT---
|
||||
digitalRead
|
||||
INPUT_PULLUP
|
||||
digitalWrite
|
||||
Serial
|
||||
---END_KEY_TEXT---
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
---LESSON_INFO---
|
||||
Pelajaran Arduino: Komunikasi Serial Monitor dan debugging.
|
||||
|
||||
**Learning Objectives:**
|
||||
- Memahami fungsi `Serial.begin()` dan baud rate
|
||||
- Menggunakan `Serial.println()` dan `Serial.print()` untuk mengirim data
|
||||
- Membedakan `println` (dengan newline) dan `print` (tanpa newline)
|
||||
- Menggunakan `millis()` untuk tracking waktu
|
||||
|
||||
**Prerequisites:**
|
||||
- Hello, World!
|
||||
---END_LESSON_INFO---
|
||||
|
||||
# Hello Serial — Komunikasi Serial Arduino
|
||||
|
||||
Serial Monitor adalah **alat debugging utama** saat bekerja dengan Arduino.
|
||||
Dengan Serial Monitor, kamu bisa mengirim teks dari Arduino ke komputer
|
||||
untuk memantau apa yang sedang terjadi dalam program.
|
||||
|
||||
## Apa itu Komunikasi Serial?
|
||||
|
||||
Arduino berkomunikasi dengan komputer melalui **port serial** (USB).
|
||||
Data dikirim bit-per-bit dengan kecepatan tertentu yang disebut **baud rate**.
|
||||
|
||||
```
|
||||
Arduino ──USB──► Komputer (Serial Monitor)
|
||||
TX/RX
|
||||
```
|
||||
|
||||
## Fungsi Serial Dasar
|
||||
|
||||
### 1. `Serial.begin(baud_rate)`
|
||||
Memulai komunikasi serial. Harus dipanggil di `setup()`:
|
||||
|
||||
```
|
||||
Serial.begin(9600); // Baud rate 9600 (paling umum)
|
||||
```
|
||||
|
||||
### 2. `Serial.println(data)`
|
||||
Mengirim data + **newline** (`\n`) di akhir:
|
||||
|
||||
```
|
||||
Serial.println("Hello"); // output: Hello↵
|
||||
Serial.println("World"); // output: World↵
|
||||
```
|
||||
|
||||
### 3. `Serial.print(data)`
|
||||
Mengirim data **tanpa newline**:
|
||||
|
||||
```
|
||||
Serial.print("Suhu: "); // output: Suhu:
|
||||
Serial.println("25 C"); // output: 25 C↵
|
||||
```
|
||||
|
||||
Hasil gabungan: `Suhu: 25 C`
|
||||
|
||||
### 4. `millis()`
|
||||
Mengembalikan jumlah milidetik sejak Arduino dinyalakan:
|
||||
|
||||
```
|
||||
unsigned long waktu = millis();
|
||||
Serial.print("Waktu: ");
|
||||
Serial.print(waktu / 1000);
|
||||
Serial.println(" detik");
|
||||
```
|
||||
|
||||
## Contoh Program
|
||||
|
||||
```
|
||||
void setup() {
|
||||
Serial.begin(9600);
|
||||
Serial.println("Arduino Ready!");
|
||||
Serial.println("Serial Monitor aktif");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
Serial.print("Uptime: ");
|
||||
Serial.print(millis() / 1000);
|
||||
Serial.println(" detik");
|
||||
delay(2000);
|
||||
}
|
||||
```
|
||||
|
||||
---EXERCISE---
|
||||
### Tantangan
|
||||
|
||||
**Tulis program Arduino** yang melakukan hal berikut saat dinyalakan (`setup`):
|
||||
|
||||
1. Mulai komunikasi serial dengan baud rate `9600`
|
||||
2. Cetak teks `Hello Arduino` (dengan newline)
|
||||
3. Cetak teks `Serial Ready` (dengan newline)
|
||||
|
||||
Kemudian di `loop()`:
|
||||
4. Cetak `Tick` setiap 1 detik
|
||||
|
||||
Setelah selesai menulis kode, tekan **Compile & Run** dan tunggu beberapa detik agar Serial output muncul.
|
||||
|
||||
> **Tips:** Lesson ini tidak memerlukan wiring — cukup tulis kode saja!
|
||||
---
|
||||
|
||||
---INITIAL_CODE_ARDUINO---
|
||||
// Hello Serial - Belajar Serial Monitor
|
||||
// Kirim pesan ke Serial Monitor
|
||||
|
||||
void setup() {
|
||||
// 1. Mulai serial dengan baud rate 9600
|
||||
|
||||
// 2. Cetak "Hello Arduino"
|
||||
|
||||
// 3. Cetak "Serial Ready"
|
||||
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// 4. Cetak "Tick" setiap 1 detik
|
||||
|
||||
}
|
||||
---END_INITIAL_CODE_ARDUINO---
|
||||
|
||||
---VELXIO_CIRCUIT---
|
||||
{
|
||||
"board": "arduino:avr:uno",
|
||||
"components": [],
|
||||
"wires": []
|
||||
}
|
||||
---END_VELXIO_CIRCUIT---
|
||||
|
||||
---EXPECTED_SERIAL_OUTPUT---
|
||||
Hello Arduino
|
||||
Serial Ready
|
||||
Tick
|
||||
---END_EXPECTED_SERIAL_OUTPUT---
|
||||
|
||||
---KEY_TEXT---
|
||||
Serial.begin
|
||||
Serial.println
|
||||
delay
|
||||
---END_KEY_TEXT---
|
||||
|
|
@ -21,6 +21,9 @@ Semua materi bisa dikerjakan langsung di browser!
|
|||
|
||||
### Arduino (Velxio)
|
||||
4. [LED Blink](lesson/led_blink_arduino.md)
|
||||
5. [Hello Serial](lesson/hello_serial_arduino.md)
|
||||
6. [Button Input](lesson/button_input_arduino.md)
|
||||
7. [Traffic Light](lesson/traffic_light_arduino.md)
|
||||
|
||||
----Available_Lessons----
|
||||
|
||||
|
|
@ -28,3 +31,6 @@ Semua materi bisa dikerjakan langsung di browser!
|
|||
2. [Variabel](lesson/variabel.md)
|
||||
3. [Rangkaian Dasar](lesson/rangkaian_dasar.md)
|
||||
4. [LED Blink](lesson/led_blink_arduino.md)
|
||||
5. [Hello Serial](lesson/hello_serial_arduino.md)
|
||||
6. [Button Input](lesson/button_input_arduino.md)
|
||||
7. [Traffic Light](lesson/traffic_light_arduino.md)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,231 @@
|
|||
---LESSON_INFO---
|
||||
Pelajaran Arduino: Membuat simulasi lampu lalu lintas dengan 3 LED.
|
||||
|
||||
**Learning Objectives:**
|
||||
- Mengontrol beberapa LED secara berurutan
|
||||
- Menerapkan pola sequence/timing dengan `delay()`
|
||||
- Merangkai 3 LED ke Arduino
|
||||
- Menggunakan `const int` untuk nama pin yang mudah dibaca
|
||||
|
||||
**Prerequisites:**
|
||||
- Hello, World!
|
||||
- LED Blink
|
||||
- Hello Serial
|
||||
- Button Input
|
||||
---END_LESSON_INFO---
|
||||
|
||||
# Traffic Light — Lampu Lalu Lintas
|
||||
|
||||
Proyek ini mensimulasikan **lampu lalu lintas** menggunakan 3 LED:
|
||||
merah, kuning, dan hijau. Kita akan mengontrol urutan nyala-mati
|
||||
LED persis seperti lampu lalu lintas sungguhan.
|
||||
|
||||
## Urutan Lampu Lalu Lintas
|
||||
|
||||
```
|
||||
1. MERAH menyala (3 detik) → Berhenti
|
||||
2. KUNING menyala (1 detik) → Bersiap
|
||||
3. HIJAU menyala (3 detik) → Jalan
|
||||
4. KUNING menyala (1 detik) → Bersiap berhenti
|
||||
5. Kembali ke MERAH...
|
||||
```
|
||||
|
||||
## Menggunakan Konstanta untuk Pin
|
||||
|
||||
Daripada mengingat nomor pin, gunakan **konstanta bernama**:
|
||||
|
||||
```
|
||||
const int RED_PIN = 13;
|
||||
const int YELLOW_PIN = 12;
|
||||
const int GREEN_PIN = 11;
|
||||
```
|
||||
|
||||
Keuntungan:
|
||||
- Kode lebih mudah dibaca: `digitalWrite(RED_PIN, HIGH)` vs `digitalWrite(13, HIGH)`
|
||||
- Jika ingin ganti pin, cukup ubah di satu tempat
|
||||
|
||||
## Mengontrol Beberapa LED
|
||||
|
||||
Setiap LED memerlukan pin output sendiri:
|
||||
|
||||
```
|
||||
void setup() {
|
||||
pinMode(RED_PIN, OUTPUT);
|
||||
pinMode(YELLOW_PIN, OUTPUT);
|
||||
pinMode(GREEN_PIN, OUTPUT);
|
||||
}
|
||||
```
|
||||
|
||||
Pastikan hanya **satu LED menyala** pada satu waktu:
|
||||
|
||||
```
|
||||
// Nyalakan merah, matikan yang lain
|
||||
digitalWrite(RED_PIN, HIGH);
|
||||
digitalWrite(YELLOW_PIN, LOW);
|
||||
digitalWrite(GREEN_PIN, LOW);
|
||||
```
|
||||
|
||||
## Serial Monitor untuk Debugging
|
||||
|
||||
Cetak status lampu ke Serial Monitor agar mudah di-debug:
|
||||
|
||||
```
|
||||
Serial.println("RED");
|
||||
delay(3000); // Merah menyala 3 detik
|
||||
```
|
||||
|
||||
---EXERCISE---
|
||||
### Tantangan
|
||||
|
||||
**Kode Arduino:**
|
||||
Buat program yang mensimulasikan lampu lalu lintas:
|
||||
1. **Merah** di pin 13 menyala selama 3 detik, cetak `RED` ke Serial
|
||||
2. **Kuning** di pin 12 menyala selama 1 detik, cetak `YELLOW` ke Serial
|
||||
3. **Hijau** di pin 11 menyala selama 3 detik, cetak `GREEN` ke Serial
|
||||
4. **Kuning** lagi selama 1 detik, cetak `YELLOW` ke Serial
|
||||
5. Ulangi dari merah
|
||||
|
||||
Pastikan hanya satu LED yang menyala pada satu waktu!
|
||||
|
||||
**Rangkaian:**
|
||||
Hubungkan 3 LED ke Arduino:
|
||||
- Pin 13 → LED Merah (Anode A), Cathode C → GND
|
||||
- Pin 12 → LED Kuning (Anode A), Cathode C → GND
|
||||
- Pin 11 → LED Hijau (Anode A), Cathode C → GND
|
||||
|
||||
Setelah selesai, tekan **Compile & Run** dan perhatikan LED menyala bergantian.
|
||||
---
|
||||
|
||||
---INITIAL_CODE_ARDUINO---
|
||||
// Traffic Light - Lampu Lalu Lintas
|
||||
// Simulasi lampu merah, kuning, hijau
|
||||
|
||||
const int RED_PIN = 13;
|
||||
const int YELLOW_PIN = 12;
|
||||
const int GREEN_PIN = 11;
|
||||
|
||||
void setup() {
|
||||
pinMode(RED_PIN, OUTPUT);
|
||||
pinMode(YELLOW_PIN, OUTPUT);
|
||||
pinMode(GREEN_PIN, OUTPUT);
|
||||
Serial.begin(9600);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Lampu MERAH
|
||||
digitalWrite(RED_PIN, HIGH);
|
||||
digitalWrite(YELLOW_PIN, LOW);
|
||||
digitalWrite(GREEN_PIN, LOW);
|
||||
Serial.println("RED");
|
||||
delay(3000);
|
||||
|
||||
// Lampu KUNING
|
||||
digitalWrite(RED_PIN, LOW);
|
||||
digitalWrite(YELLOW_PIN, HIGH);
|
||||
digitalWrite(GREEN_PIN, LOW);
|
||||
Serial.println("YELLOW");
|
||||
delay(1000);
|
||||
|
||||
// Lampu HIJAU
|
||||
digitalWrite(RED_PIN, LOW);
|
||||
digitalWrite(YELLOW_PIN, LOW);
|
||||
digitalWrite(GREEN_PIN, HIGH);
|
||||
Serial.println("GREEN");
|
||||
delay(3000);
|
||||
|
||||
// Lampu KUNING lagi
|
||||
digitalWrite(RED_PIN, LOW);
|
||||
digitalWrite(YELLOW_PIN, HIGH);
|
||||
digitalWrite(GREEN_PIN, LOW);
|
||||
Serial.println("YELLOW");
|
||||
delay(1000);
|
||||
}
|
||||
---END_INITIAL_CODE_ARDUINO---
|
||||
|
||||
---VELXIO_CIRCUIT---
|
||||
{
|
||||
"board": "arduino:avr:uno",
|
||||
"components": [
|
||||
{
|
||||
"type": "wokwi-led",
|
||||
"id": "led-red",
|
||||
"x": 370,
|
||||
"y": -300,
|
||||
"rotation": 0,
|
||||
"props": {
|
||||
"color": "red",
|
||||
"pin": 13
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "wokwi-led",
|
||||
"id": "led-yellow",
|
||||
"x": 370,
|
||||
"y": -200,
|
||||
"rotation": 0,
|
||||
"props": {
|
||||
"color": "yellow",
|
||||
"pin": 12
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "wokwi-led",
|
||||
"id": "led-green",
|
||||
"x": 370,
|
||||
"y": -100,
|
||||
"rotation": 0,
|
||||
"props": {
|
||||
"color": "green",
|
||||
"pin": 11
|
||||
}
|
||||
}
|
||||
],
|
||||
"wires": []
|
||||
}
|
||||
---END_VELXIO_CIRCUIT---
|
||||
|
||||
---EXPECTED_SERIAL_OUTPUT---
|
||||
RED
|
||||
YELLOW
|
||||
GREEN
|
||||
YELLOW
|
||||
---END_EXPECTED_SERIAL_OUTPUT---
|
||||
|
||||
---EXPECTED_WIRING---
|
||||
{
|
||||
"wires": [
|
||||
{
|
||||
"start": { "componentId": "arduino-uno", "pinName": "13" },
|
||||
"end": { "componentId": "led-red", "pinName": "A" }
|
||||
},
|
||||
{
|
||||
"start": { "componentId": "led-red", "pinName": "C" },
|
||||
"end": { "componentId": "arduino-uno", "pinName": "GND" }
|
||||
},
|
||||
{
|
||||
"start": { "componentId": "arduino-uno", "pinName": "12" },
|
||||
"end": { "componentId": "led-yellow", "pinName": "A" }
|
||||
},
|
||||
{
|
||||
"start": { "componentId": "led-yellow", "pinName": "C" },
|
||||
"end": { "componentId": "arduino-uno", "pinName": "GND" }
|
||||
},
|
||||
{
|
||||
"start": { "componentId": "arduino-uno", "pinName": "11" },
|
||||
"end": { "componentId": "led-green", "pinName": "A" }
|
||||
},
|
||||
{
|
||||
"start": { "componentId": "led-green", "pinName": "C" },
|
||||
"end": { "componentId": "arduino-uno", "pinName": "GND" }
|
||||
}
|
||||
]
|
||||
}
|
||||
---END_EXPECTED_WIRING---
|
||||
|
||||
---KEY_TEXT---
|
||||
pinMode
|
||||
digitalWrite
|
||||
const int
|
||||
Serial
|
||||
delay
|
||||
---END_KEY_TEXT---
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
# Elemes Load Test
|
||||
|
||||
Load test E2E menggunakan [Locust](https://locust.io/) yang otomatis di-generate dari folder `content/`.
|
||||
|
||||
## Cara Pakai
|
||||
|
||||
```bash
|
||||
cd elemes/load-test
|
||||
|
||||
# 0. Virtual Environment
|
||||
python3 -m venv ./env
|
||||
source ./env/bin/activate
|
||||
|
||||
# 1. Install dependency
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 2. Generate test data dari content/
|
||||
python content_parser.py
|
||||
|
||||
# 3. Jalankan Locust (opsional: set VELXIO_HOST jika Velxio bukan di localhost:8001)
|
||||
export VELXIO_HOST=http://localhost:8001
|
||||
locust -f locustfile.py
|
||||
```
|
||||
|
||||
Buka **http://localhost:8089**, masukkan URL backend Elemes (misalnya `http://localhost:5000`), lalu mulai test.
|
||||
|
||||
## File
|
||||
|
||||
| File | Fungsi |
|
||||
|------|--------|
|
||||
| `content_parser.py` | Parse `content/*.md` → `test_data.json` + inject token test ke CSV |
|
||||
| `locustfile.py` | Locust script (7 task weighted) yang baca `test_data.json` |
|
||||
| `test_data.json` | Auto-generated, **jangan di-commit** |
|
||||
| `requirements.txt` | Dependency (`locust`) |
|
||||
|
||||
## Test Scenarios (8 Tasks)
|
||||
|
||||
| # | Task | Weight | Target | Deskripsi |
|
||||
|---|------|--------|--------|----------|
|
||||
| 1 | Browse Lessons | 3 | Elemes | GET `/lessons`, validasi jumlah lesson |
|
||||
| 2 | View Detail | 5 | Elemes | GET `/lesson/{slug}.json`, validasi field per tipe |
|
||||
| 3 | Compile C | 4 | Elemes | POST `/compile`, validasi output vs expected |
|
||||
| 4 | Compile Python | 3 | Elemes | POST `/compile`, validasi output vs expected |
|
||||
| 5 | Verify Arduino | 2 | Elemes | GET lesson Arduino, validasi JSON structure |
|
||||
| 6 | Complete Flow | 2 | Both | fetch → compile → track-progress |
|
||||
| 7 | Progress Report | 1 | Elemes | Login guru → GET `/progress-report.json` |
|
||||
| 8 | **Compile Arduino** | **3** | **Velxio** | POST `/api/compile`, validasi hex_content |
|
||||
|
||||
## Re-generate Setelah Tambah Lesson Baru
|
||||
|
||||
Setiap kali ada lesson baru di `content/`, cukup jalankan ulang:
|
||||
|
||||
```bash
|
||||
python content_parser.py
|
||||
```
|
||||
|
||||
`test_data.json` akan di-update otomatis dan Locust langsung test lesson baru.
|
||||
|
|
@ -0,0 +1,306 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Content Parser for Locust E2E Test Generation.
|
||||
|
||||
Scans the content/ directory, parses lesson markdown files,
|
||||
extracts test-relevant data, and writes test_data.json.
|
||||
|
||||
Also injects a LOCUST_TEST token into tokens_siswa.csv if not present.
|
||||
|
||||
Usage (from elemes/load-test/):
|
||||
python content_parser.py
|
||||
python content_parser.py --content-dir ../../content --tokens-file ../../tokens_siswa.csv --num-tokens 50
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Marker extraction (mirrors lesson_service.py logic)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def extract_section(content: str, start_marker: str, end_marker: str) -> tuple[str, str]:
|
||||
"""Extract text between markers. Returns (extracted, remaining)."""
|
||||
if start_marker not in content or end_marker not in content:
|
||||
return "", content
|
||||
|
||||
start_idx = content.find(start_marker)
|
||||
end_idx = content.find(end_marker)
|
||||
if start_idx == -1 or end_idx == -1 or end_idx <= start_idx:
|
||||
return "", content
|
||||
|
||||
extracted = content[start_idx + len(start_marker):end_idx].strip()
|
||||
remaining = content[:start_idx] + content[end_idx + len(end_marker):]
|
||||
return extracted, remaining
|
||||
|
||||
|
||||
def detect_lesson_type(content: str) -> str:
|
||||
"""Detect lesson type from markers present in content."""
|
||||
has_arduino = '---INITIAL_CODE_ARDUINO---' in content
|
||||
has_c = '---INITIAL_CODE---' in content
|
||||
has_python = '---INITIAL_PYTHON---' in content
|
||||
has_circuit = '---INITIAL_CIRCUIT---' in content
|
||||
has_quiz = '---INITIAL_QUIZ---' in content
|
||||
has_velxio = '---VELXIO_CIRCUIT---' in content
|
||||
|
||||
if has_arduino or (has_velxio and not has_c and not has_python):
|
||||
return 'arduino'
|
||||
if has_quiz:
|
||||
return 'quiz'
|
||||
if has_c and has_circuit:
|
||||
return 'hybrid'
|
||||
if has_circuit and not has_c and not has_python:
|
||||
return 'circuit'
|
||||
if has_python and not has_c:
|
||||
return 'python'
|
||||
return 'c'
|
||||
|
||||
|
||||
def parse_lesson(filepath: str) -> dict:
|
||||
"""Parse a single lesson markdown file and extract test data."""
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
slug = os.path.basename(filepath).replace('.md', '')
|
||||
lesson_type = detect_lesson_type(content)
|
||||
|
||||
# Extract all relevant sections
|
||||
initial_code_c, _ = extract_section(content, '---INITIAL_CODE---', '---END_INITIAL_CODE---')
|
||||
initial_python, _ = extract_section(content, '---INITIAL_PYTHON---', '---END_INITIAL_PYTHON---')
|
||||
initial_code_arduino, _ = extract_section(content, '---INITIAL_CODE_ARDUINO---', '---END_INITIAL_CODE_ARDUINO---')
|
||||
velxio_circuit, _ = extract_section(content, '---VELXIO_CIRCUIT---', '---END_VELXIO_CIRCUIT---')
|
||||
expected_output, _ = extract_section(content, '---EXPECTED_OUTPUT---', '---END_EXPECTED_OUTPUT---')
|
||||
expected_output_python, _ = extract_section(content, '---EXPECTED_OUTPUT_PYTHON---', '---END_EXPECTED_OUTPUT_PYTHON---')
|
||||
expected_serial, _ = extract_section(content, '---EXPECTED_SERIAL_OUTPUT---', '---END_EXPECTED_SERIAL_OUTPUT---')
|
||||
expected_wiring, _ = extract_section(content, '---EXPECTED_WIRING---', '---END_EXPECTED_WIRING---')
|
||||
key_text, _ = extract_section(content, '---KEY_TEXT---', '---END_KEY_TEXT---')
|
||||
solution_code, _ = extract_section(content, '---SOLUTION_CODE---', '---END_SOLUTION_CODE---')
|
||||
solution_python, _ = extract_section(content, '---SOLUTION_PYTHON---', '---END_SOLUTION_PYTHON---')
|
||||
|
||||
# A lesson is compilable if it has solution code or Arduino initial code
|
||||
is_compilable = bool(solution_code or solution_python or initial_code_arduino)
|
||||
|
||||
data = {
|
||||
'slug': slug,
|
||||
'type': lesson_type,
|
||||
'has_c': bool(initial_code_c),
|
||||
'has_python': bool(initial_python),
|
||||
'has_circuit': '---INITIAL_CIRCUIT---' in content,
|
||||
'has_arduino': bool(initial_code_arduino),
|
||||
'has_velxio': bool(velxio_circuit),
|
||||
'compilable': is_compilable,
|
||||
'key_text': key_text,
|
||||
}
|
||||
|
||||
# Add type-specific fields
|
||||
if initial_code_c:
|
||||
data['initial_code_c'] = initial_code_c
|
||||
if solution_code:
|
||||
data['solution_code'] = solution_code
|
||||
if initial_python:
|
||||
data['initial_python'] = initial_python
|
||||
if solution_python:
|
||||
data['solution_python'] = solution_python
|
||||
if expected_output:
|
||||
data['expected_output'] = expected_output
|
||||
if expected_output_python:
|
||||
data['expected_output_python'] = expected_output_python
|
||||
if initial_code_arduino:
|
||||
data['initial_code_arduino'] = initial_code_arduino
|
||||
if velxio_circuit:
|
||||
data['velxio_circuit'] = velxio_circuit
|
||||
if expected_serial:
|
||||
data['expected_serial'] = expected_serial
|
||||
if expected_wiring:
|
||||
data['expected_wiring'] = expected_wiring
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_ordered_slugs(content_dir: str) -> list[str]:
|
||||
"""Get lesson slugs in order from home.md's Available_Lessons section."""
|
||||
home_path = os.path.join(content_dir, 'home.md')
|
||||
if not os.path.exists(home_path):
|
||||
return []
|
||||
|
||||
with open(home_path, 'r', encoding='utf-8') as f:
|
||||
home_content = f.read()
|
||||
|
||||
parts = home_content.split('----Available_Lessons----')
|
||||
if len(parts) <= 1:
|
||||
# Try alternate separator
|
||||
parts = home_content.split('---Available_Lessons---')
|
||||
if len(parts) <= 1:
|
||||
return []
|
||||
|
||||
links = re.findall(r'\[([^\]]+)\]\((?:lesson/)?([^\)]+)\)', parts[1])
|
||||
return [fn.replace('.md', '') for _, fn in links]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Token injection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def ensure_test_tokens(tokens_file: str, lesson_slugs: list[str], num_tokens: int) -> list[str]:
|
||||
"""
|
||||
Ensure a specified number of test tokens exist in tokens_siswa.csv.
|
||||
Returns a list of token strings.
|
||||
"""
|
||||
existing_tokens = []
|
||||
rows = []
|
||||
fieldnames = []
|
||||
|
||||
if os.path.exists(tokens_file):
|
||||
with open(tokens_file, 'r', newline='', encoding='utf-8') as f:
|
||||
reader = csv.DictReader(f, delimiter=';')
|
||||
fieldnames = reader.fieldnames or []
|
||||
rows = list(reader)
|
||||
|
||||
for row in rows:
|
||||
if row.get('token', '').startswith('LOCUST_TEST_'):
|
||||
existing_tokens.append(row['token'])
|
||||
|
||||
if not fieldnames:
|
||||
fieldnames = ['token', 'nama_siswa'] + lesson_slugs
|
||||
|
||||
tokens_to_return = existing_tokens[:num_tokens]
|
||||
|
||||
if len(tokens_to_return) < num_tokens:
|
||||
needed = num_tokens - len(tokens_to_return)
|
||||
new_tokens = []
|
||||
for i in range(needed):
|
||||
token = f"LOCUST_TEST_{uuid.uuid4().hex[:8]}"
|
||||
new_tokens.append(token)
|
||||
|
||||
new_row = {'token': token, 'nama_siswa': f"Locust Bot {len(tokens_to_return) + i + 1}"}
|
||||
for slug in lesson_slugs:
|
||||
if slug not in fieldnames:
|
||||
fieldnames.append(slug)
|
||||
new_row[slug] = 'not_started'
|
||||
rows.append(new_row)
|
||||
|
||||
with open(tokens_file, 'w', newline='', encoding='utf-8') as f:
|
||||
writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter=';')
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
|
||||
print(f" ✚ Injected {len(new_tokens)} new test tokens.")
|
||||
tokens_to_return.extend(new_tokens)
|
||||
else:
|
||||
print(f" ♻ Reusing {num_tokens} existing test tokens.")
|
||||
|
||||
return tokens_to_return
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _get_teacher_token(tokens_file: str) -> str:
|
||||
"""Get the teacher token (first data row)."""
|
||||
if not os.path.exists(tokens_file):
|
||||
return ""
|
||||
with open(tokens_file, 'r', newline='', encoding='utf-8') as f:
|
||||
reader = csv.DictReader(f, delimiter=';')
|
||||
for row in reader:
|
||||
return row.get('token', '')
|
||||
return ""
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Parse content/ for Locust test generation')
|
||||
parser.add_argument('--content-dir', default='../../content',
|
||||
help='Path to content directory (default: ../../content)')
|
||||
parser.add_argument('--tokens-file', default='../../tokens_siswa.csv',
|
||||
help='Path to tokens CSV (default: ../../tokens_siswa.csv)')
|
||||
parser.add_argument('--num-tokens', type=int, default=50,
|
||||
help='Number of test tokens to generate (default: 50)')
|
||||
parser.add_argument('--output', default='test_data.json',
|
||||
help='Output JSON file (default: test_data.json)')
|
||||
args = parser.parse_args()
|
||||
|
||||
content_dir = os.path.abspath(args.content_dir)
|
||||
tokens_file = os.path.abspath(args.tokens_file)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" Elemes Content Parser for Locust E2E Testing")
|
||||
print(f"{'='*60}")
|
||||
print(f" Content dir : {content_dir}")
|
||||
print(f" Tokens file : {tokens_file}")
|
||||
print(f" Num tokens : {args.num_tokens}")
|
||||
print(f" Output : {args.output}")
|
||||
print()
|
||||
|
||||
# 1. Get ordered lesson slugs
|
||||
ordered_slugs = get_ordered_slugs(content_dir)
|
||||
if not ordered_slugs:
|
||||
# Fallback: scan directory
|
||||
ordered_slugs = [
|
||||
f.replace('.md', '')
|
||||
for f in sorted(os.listdir(content_dir))
|
||||
if f.endswith('.md') and f != 'home.md'
|
||||
]
|
||||
|
||||
print(f" 📚 Found {len(ordered_slugs)} lessons:")
|
||||
|
||||
# 2. Parse each lesson
|
||||
lessons = []
|
||||
for slug in ordered_slugs:
|
||||
filepath = os.path.join(content_dir, f'{slug}.md')
|
||||
if not os.path.exists(filepath):
|
||||
print(f" ⚠ {slug}.md not found, skipping")
|
||||
continue
|
||||
|
||||
lesson = parse_lesson(filepath)
|
||||
lessons.append(lesson)
|
||||
|
||||
# Summary icon per type
|
||||
icons = {
|
||||
'c': '🔧', 'python': '🐍', 'hybrid': '🔀',
|
||||
'circuit': '⚡', 'arduino': '🤖', 'quiz': '❓'
|
||||
}
|
||||
icon = icons.get(lesson['type'], '📄')
|
||||
compilable = '✓ compile' if lesson.get('compilable') else '✗ compile'
|
||||
print(f" {icon} {slug} [{lesson['type']}] {compilable}")
|
||||
|
||||
# 3. Inject test tokens
|
||||
print()
|
||||
tokens = ensure_test_tokens(tokens_file, ordered_slugs, args.num_tokens)
|
||||
|
||||
# 4. Build output
|
||||
test_data = {
|
||||
'generated_by': 'content_parser.py',
|
||||
'tokens': tokens,
|
||||
'teacher_token': _get_teacher_token(tokens_file),
|
||||
'lessons': lessons,
|
||||
'stats': {
|
||||
'total': len(lessons),
|
||||
'c': sum(1 for l in lessons if l['type'] == 'c'),
|
||||
'python': sum(1 for l in lessons if l['type'] == 'python'),
|
||||
'hybrid': sum(1 for l in lessons if l['type'] == 'hybrid'),
|
||||
'circuit': sum(1 for l in lessons if l['type'] == 'circuit'),
|
||||
'arduino': sum(1 for l in lessons if l['type'] == 'arduino'),
|
||||
'quiz': sum(1 for l in lessons if l['type'] == 'quiz'),
|
||||
'compilable': sum(1 for l in lessons if l.get('compilable')),
|
||||
}
|
||||
}
|
||||
|
||||
# 5. Write output
|
||||
with open(args.output, 'w', encoding='utf-8') as f:
|
||||
json.dump(test_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"\n ✅ Wrote {args.output}")
|
||||
print(f" {test_data['stats']['total']} lessons "
|
||||
f"({test_data['stats']['compilable']} compilable)")
|
||||
print(f"\n Next: locust -f locustfile.py")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,482 @@
|
|||
"""
|
||||
Locust E2E Test Suite for Elemes LMS.
|
||||
|
||||
Auto-generated test scenarios from content/ folder via content_parser.py.
|
||||
Reads test_data.json to know which lessons exist and their types.
|
||||
|
||||
Usage (from elemes/load-test/):
|
||||
1. python content_parser.py # generate test_data.json
|
||||
2. locust -f locustfile.py # open web UI at http://localhost:8089
|
||||
→ Set host to your Elemes backend URL in the web UI
|
||||
|
||||
Environment variables:
|
||||
VELXIO_HOST — Velxio backend URL for Arduino compilation
|
||||
(default: http://localhost:8001)
|
||||
|
||||
All API paths hit the Flask backend directly (no /api prefix needed
|
||||
when targeting Flask at :5000). Arduino compile hits Velxio backend.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
|
||||
import requests
|
||||
from locust import HttpUser, task, between, events
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Load test data
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
TEST_DATA_FILE = os.path.join(os.path.dirname(__file__), 'test_data.json')
|
||||
|
||||
def load_test_data() -> dict:
|
||||
"""Load test_data.json generated by content_parser.py."""
|
||||
if not os.path.exists(TEST_DATA_FILE):
|
||||
raise FileNotFoundError(
|
||||
f"{TEST_DATA_FILE} not found. "
|
||||
f"Run 'python content_parser.py' first."
|
||||
)
|
||||
with open(TEST_DATA_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
try:
|
||||
TEST_DATA = load_test_data()
|
||||
except FileNotFoundError as e:
|
||||
logging.error(str(e))
|
||||
TEST_DATA = {'token': '', 'teacher_token': '', 'lessons': []}
|
||||
|
||||
TOKENS = TEST_DATA.get('tokens', [])
|
||||
TEACHER_TOKEN = TEST_DATA.get('teacher_token', '')
|
||||
ALL_LESSONS = TEST_DATA.get('lessons', [])
|
||||
|
||||
# Velxio backend URL for Arduino compilation
|
||||
VELXIO_HOST = os.environ.get('VELXIO_HOST', 'http://localhost:8001')
|
||||
|
||||
# Pre-filter lessons by type for task assignment
|
||||
C_LESSONS = [l for l in ALL_LESSONS if l['type'] == 'c' and l.get('solution_code')]
|
||||
PYTHON_LESSONS = [l for l in ALL_LESSONS if l['type'] == 'python' and l.get('solution_python')]
|
||||
HYBRID_LESSONS = [l for l in ALL_LESSONS if l['type'] == 'hybrid']
|
||||
ARDUINO_LESSONS = [l for l in ALL_LESSONS if l['type'] == 'arduino']
|
||||
COMPILABLE_LESSONS = C_LESSONS + PYTHON_LESSONS + [
|
||||
l for l in HYBRID_LESSONS if l.get('solution_code') or l.get('solution_python')
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def check_key_text(code: str, key_text: str) -> bool:
|
||||
"""Client-side key_text check (mirrors exercise.ts logic)."""
|
||||
if not key_text.strip():
|
||||
return True
|
||||
keys = [k.strip() for k in key_text.split('\n') if k.strip()]
|
||||
return all(k in code for k in keys)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Locust User
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class ElemesStudent(HttpUser):
|
||||
"""
|
||||
Simulates a student interacting with the Elemes LMS.
|
||||
|
||||
Each user logs in once (on_start), then performs weighted tasks
|
||||
that mirror real student behavior.
|
||||
"""
|
||||
|
||||
wait_time = between(1, 5)
|
||||
|
||||
def on_start(self):
|
||||
"""Login once when user spawns."""
|
||||
self.token = random.choice(TOKENS) if TOKENS else ''
|
||||
resp = self.client.post(
|
||||
'/login',
|
||||
json={'token': self.token},
|
||||
name='/login'
|
||||
)
|
||||
data = resp.json()
|
||||
if not data.get('success'):
|
||||
logging.warning(f"Login failed: {data.get('message')}")
|
||||
|
||||
# ── Task 1: Browse Lessons (weight=3) ──────────────────────────────
|
||||
|
||||
@task(3)
|
||||
def browse_lessons(self):
|
||||
"""Fetch lesson list — simulates landing on home page."""
|
||||
with self.client.get('/lessons', name='/lessons', catch_response=True) as resp:
|
||||
data = resp.json()
|
||||
lessons = data.get('lessons', [])
|
||||
if len(lessons) == 0:
|
||||
resp.failure("No lessons returned")
|
||||
elif len(lessons) != len(ALL_LESSONS):
|
||||
resp.failure(
|
||||
f"Expected {len(ALL_LESSONS)} lessons, got {len(lessons)}"
|
||||
)
|
||||
|
||||
# ── Task 2: View Lesson Detail (weight=5) ──────────────────────────
|
||||
|
||||
@task(5)
|
||||
def view_lesson_detail(self):
|
||||
"""Fetch a random lesson's detail JSON."""
|
||||
if not ALL_LESSONS:
|
||||
return
|
||||
|
||||
lesson = random.choice(ALL_LESSONS)
|
||||
slug = lesson['slug']
|
||||
|
||||
with self.client.get(
|
||||
f'/lesson/{slug}.json',
|
||||
name='/lesson/[slug].json',
|
||||
catch_response=True
|
||||
) as resp:
|
||||
data = resp.json()
|
||||
|
||||
if 'error' in data:
|
||||
resp.failure(f"Lesson {slug} not found")
|
||||
return
|
||||
|
||||
# Validate fields based on type
|
||||
if lesson['type'] == 'arduino':
|
||||
if not data.get('initial_code_arduino'):
|
||||
resp.failure(f"{slug}: missing initial_code_arduino")
|
||||
if not data.get('velxio_circuit'):
|
||||
resp.failure(f"{slug}: missing velxio_circuit")
|
||||
# Validate velxio_circuit is valid JSON
|
||||
try:
|
||||
json.loads(data['velxio_circuit'])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
resp.failure(f"{slug}: velxio_circuit is not valid JSON")
|
||||
elif lesson['type'] in ('c', 'hybrid'):
|
||||
if not data.get('initial_code'):
|
||||
resp.failure(f"{slug}: missing initial_code")
|
||||
elif lesson['type'] == 'python':
|
||||
if not data.get('initial_python') and not data.get('initial_code'):
|
||||
resp.failure(f"{slug}: missing initial code")
|
||||
|
||||
# ── Task 3: Compile C Lesson (weight=4) ────────────────────────────
|
||||
|
||||
@task(4)
|
||||
def compile_c_lesson(self):
|
||||
"""Compile a C lesson's solution and validate output."""
|
||||
if not C_LESSONS:
|
||||
return
|
||||
|
||||
lesson = random.choice(C_LESSONS)
|
||||
code = lesson.get('solution_code', lesson.get('initial_code_c', ''))
|
||||
expected = lesson.get('expected_output', '')
|
||||
|
||||
if not code:
|
||||
return
|
||||
|
||||
with self.client.post(
|
||||
'/compile',
|
||||
json={'code': code, 'language': 'c'},
|
||||
name='/compile [C]',
|
||||
catch_response=True
|
||||
) as resp:
|
||||
data = resp.json()
|
||||
|
||||
if not data.get('success'):
|
||||
resp.failure(f"Compile failed: {data.get('error', 'unknown')}")
|
||||
return
|
||||
|
||||
output = data.get('output', '')
|
||||
|
||||
# Validate expected output
|
||||
if expected and expected.strip() not in output:
|
||||
resp.failure(
|
||||
f"{lesson['slug']}: expected '{expected.strip()[:50]}' "
|
||||
f"not in output '{output[:50]}'"
|
||||
)
|
||||
return
|
||||
|
||||
# Validate key_text
|
||||
key_text = lesson.get('key_text', '')
|
||||
if key_text and not check_key_text(code, key_text):
|
||||
resp.failure(f"{lesson['slug']}: key_text check failed")
|
||||
|
||||
# ── Task 4: Compile Python Lesson (weight=3) ───────────────────────
|
||||
|
||||
@task(3)
|
||||
def compile_python_lesson(self):
|
||||
"""Compile a Python lesson's solution and validate output."""
|
||||
if not PYTHON_LESSONS:
|
||||
return
|
||||
|
||||
lesson = random.choice(PYTHON_LESSONS)
|
||||
code = lesson.get('solution_python', lesson.get('initial_python', ''))
|
||||
expected = lesson.get('expected_output_python', lesson.get('expected_output', ''))
|
||||
|
||||
if not code:
|
||||
return
|
||||
|
||||
with self.client.post(
|
||||
'/compile',
|
||||
json={'code': code, 'language': 'python'},
|
||||
name='/compile [Python]',
|
||||
catch_response=True
|
||||
) as resp:
|
||||
data = resp.json()
|
||||
|
||||
if not data.get('success'):
|
||||
resp.failure(f"Compile failed: {data.get('error', 'unknown')}")
|
||||
return
|
||||
|
||||
output = data.get('output', '')
|
||||
if expected and expected.strip() not in output:
|
||||
resp.failure(
|
||||
f"{lesson['slug']}: expected '{expected.strip()[:50]}' "
|
||||
f"not in output '{output[:50]}'"
|
||||
)
|
||||
|
||||
# ── Task 5: Verify Arduino Lesson Structure (weight=2) ─────────────
|
||||
|
||||
@task(2)
|
||||
def verify_arduino_lesson(self):
|
||||
"""
|
||||
Verify an Arduino lesson's data structure.
|
||||
Validates JSON integrity of velxio_circuit and expected_wiring.
|
||||
"""
|
||||
if not ARDUINO_LESSONS:
|
||||
return
|
||||
|
||||
lesson = random.choice(ARDUINO_LESSONS)
|
||||
slug = lesson['slug']
|
||||
|
||||
with self.client.get(
|
||||
f'/lesson/{slug}.json',
|
||||
name='/lesson/[arduino].json',
|
||||
catch_response=True
|
||||
) as resp:
|
||||
data = resp.json()
|
||||
|
||||
errors = []
|
||||
|
||||
# Check required fields
|
||||
if not data.get('initial_code_arduino'):
|
||||
errors.append('missing initial_code_arduino')
|
||||
|
||||
vc = data.get('velxio_circuit', '')
|
||||
if vc:
|
||||
try:
|
||||
circuit = json.loads(vc)
|
||||
if 'board' not in circuit:
|
||||
errors.append('velxio_circuit missing board field')
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
errors.append('velxio_circuit invalid JSON')
|
||||
else:
|
||||
errors.append('missing velxio_circuit')
|
||||
|
||||
# Check expected_wiring JSON validity if present
|
||||
ew = data.get('expected_wiring', '')
|
||||
if ew:
|
||||
try:
|
||||
wiring = json.loads(ew)
|
||||
if 'wires' not in wiring:
|
||||
errors.append('expected_wiring missing wires array')
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
errors.append('expected_wiring invalid JSON')
|
||||
|
||||
# Key text check against initial code
|
||||
key_text = lesson.get('key_text', '')
|
||||
initial_code = data.get('initial_code_arduino', '')
|
||||
if key_text and initial_code and not check_key_text(initial_code, key_text):
|
||||
errors.append('key_text not found in initial_code_arduino')
|
||||
|
||||
if errors:
|
||||
resp.failure(f"{slug}: {'; '.join(errors)}")
|
||||
|
||||
# ── Task 8: Compile Arduino via Velxio (weight=3) ──────────────────
|
||||
|
||||
@task(3)
|
||||
def compile_arduino_lesson(self):
|
||||
"""
|
||||
Compile an Arduino lesson's code via Velxio backend.
|
||||
Hits Velxio's POST /api/compile endpoint.
|
||||
Validates: compile success + hex_content returned.
|
||||
Note: serial output can only be validated in-browser (avr8js sim).
|
||||
"""
|
||||
if not ARDUINO_LESSONS:
|
||||
return
|
||||
|
||||
lesson = random.choice(ARDUINO_LESSONS)
|
||||
code = lesson.get('initial_code_arduino', '')
|
||||
if not code:
|
||||
return
|
||||
|
||||
try:
|
||||
resp = requests.post(
|
||||
f'{VELXIO_HOST}/api/compile',
|
||||
json={'code': code, 'board_fqbn': 'arduino:avr:uno'},
|
||||
timeout=30
|
||||
)
|
||||
data = resp.json()
|
||||
|
||||
if not data.get('success'):
|
||||
# Report as Locust failure for stats tracking
|
||||
self.environment.events.request.fire(
|
||||
request_type='POST',
|
||||
name='/api/compile [Arduino]',
|
||||
response_time=resp.elapsed.total_seconds() * 1000,
|
||||
response_length=len(resp.content),
|
||||
exception=Exception(
|
||||
f"{lesson['slug']}: compile failed — "
|
||||
f"{data.get('error', data.get('stderr', 'unknown'))}"
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
hex_content = data.get('hex_content', '')
|
||||
if not hex_content:
|
||||
self.environment.events.request.fire(
|
||||
request_type='POST',
|
||||
name='/api/compile [Arduino]',
|
||||
response_time=resp.elapsed.total_seconds() * 1000,
|
||||
response_length=len(resp.content),
|
||||
exception=Exception(f"{lesson['slug']}: no hex_content"),
|
||||
)
|
||||
return
|
||||
|
||||
# Success
|
||||
self.environment.events.request.fire(
|
||||
request_type='POST',
|
||||
name='/api/compile [Arduino]',
|
||||
response_time=resp.elapsed.total_seconds() * 1000,
|
||||
response_length=len(resp.content),
|
||||
exception=None,
|
||||
)
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.environment.events.request.fire(
|
||||
request_type='POST',
|
||||
name='/api/compile [Arduino]',
|
||||
response_time=0,
|
||||
response_length=0,
|
||||
exception=e,
|
||||
)
|
||||
|
||||
# ── Task 6: Complete Lesson Flow (weight=2) ────────────────────────
|
||||
|
||||
@task(2)
|
||||
def complete_lesson_flow(self):
|
||||
"""
|
||||
Full flow: fetch lesson → compile (if possible) → track progress.
|
||||
Simulates a student completing a lesson.
|
||||
"""
|
||||
if not ALL_LESSONS:
|
||||
return
|
||||
|
||||
lesson = random.choice(ALL_LESSONS)
|
||||
slug = lesson['slug']
|
||||
|
||||
# 1. Fetch lesson detail
|
||||
resp = self.client.get(
|
||||
f'/lesson/{slug}.json',
|
||||
name='/lesson/[slug].json (flow)'
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
return
|
||||
|
||||
# 2. Compile if it's a C or Python lesson with solution
|
||||
if lesson['type'] in ('c', 'hybrid') and lesson.get('solution_code'):
|
||||
self.client.post(
|
||||
'/compile',
|
||||
json={'code': lesson['solution_code'], 'language': 'c'},
|
||||
name='/compile (flow)'
|
||||
)
|
||||
time.sleep(0.5) # Simulate student reading output
|
||||
|
||||
elif lesson['type'] == 'python' and lesson.get('solution_python'):
|
||||
self.client.post(
|
||||
'/compile',
|
||||
json={'code': lesson['solution_python'], 'language': 'python'},
|
||||
name='/compile (flow)'
|
||||
)
|
||||
time.sleep(0.5)
|
||||
|
||||
elif lesson['type'] == 'arduino' and lesson.get('initial_code_arduino'):
|
||||
try:
|
||||
requests.post(
|
||||
f'{VELXIO_HOST}/api/compile',
|
||||
json={'code': lesson['initial_code_arduino'],
|
||||
'board_fqbn': 'arduino:avr:uno'},
|
||||
timeout=30
|
||||
)
|
||||
except requests.RequestException:
|
||||
pass # Non-critical in flow
|
||||
time.sleep(0.5)
|
||||
|
||||
# 3. Track progress
|
||||
with self.client.post(
|
||||
'/track-progress',
|
||||
json={
|
||||
'token': self.token,
|
||||
'lesson_name': slug,
|
||||
'status': 'completed'
|
||||
},
|
||||
name='/track-progress',
|
||||
catch_response=True
|
||||
) as resp:
|
||||
data = resp.json()
|
||||
if not data.get('success'):
|
||||
resp.failure(f"Track progress failed: {data.get('message')}")
|
||||
|
||||
# ── Task 7: Progress Report (weight=1) ─────────────────────────────
|
||||
|
||||
@task(1)
|
||||
def view_progress_report(self):
|
||||
"""Fetch progress report (teacher perspective)."""
|
||||
if not TEACHER_TOKEN:
|
||||
return
|
||||
|
||||
# Login as teacher
|
||||
self.client.post(
|
||||
'/login',
|
||||
json={'token': TEACHER_TOKEN},
|
||||
name='/login [teacher]'
|
||||
)
|
||||
|
||||
with self.client.get(
|
||||
'/progress-report.json',
|
||||
name='/progress-report.json',
|
||||
catch_response=True
|
||||
) as resp:
|
||||
data = resp.json()
|
||||
students = data.get('students', [])
|
||||
lessons = data.get('lessons', [])
|
||||
|
||||
if not lessons:
|
||||
resp.failure("No lessons in progress report")
|
||||
|
||||
# Re-login as student for subsequent tasks
|
||||
self.client.post(
|
||||
'/login',
|
||||
json={'token': self.token},
|
||||
name='/login [re-auth]'
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Event hooks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@events.test_start.add_listener
|
||||
def on_test_start(environment, **kwargs):
|
||||
"""Log test configuration at start."""
|
||||
stats = TEST_DATA.get('stats', {})
|
||||
logging.info("=" * 60)
|
||||
logging.info(" Elemes LMS — Locust E2E Test")
|
||||
logging.info("=" * 60)
|
||||
logging.info(f" Lessons: {stats.get('total', 0)} total")
|
||||
logging.info(f" C: {stats.get('c', 0)}, Python: {stats.get('python', 0)}, "
|
||||
f"Hybrid: {stats.get('hybrid', 0)}, Arduino: {stats.get('arduino', 0)}")
|
||||
logging.info(f" Compilable: {stats.get('compilable', 0)}")
|
||||
logging.info(f" Tokens: {len(TOKENS)} loaded")
|
||||
logging.info(f" Velxio: {VELXIO_HOST}")
|
||||
logging.info("=" * 60)
|
||||
|
|
@ -0,0 +1 @@
|
|||
locust
|
||||
|
|
@ -73,5 +73,4 @@ volumes:
|
|||
|
||||
networks:
|
||||
main_network:
|
||||
drive: bridge
|
||||
network_mode: service:elemes-ts
|
||||
driver: bridge
|
||||
|
|
|
|||
1117
proposal.md
1117
proposal.md
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue