Testar código Arduino no Jobe (sem Moodle)
O Jobe executa código no compilador do host (g++), não no ESP32. Então há uma fronteira clara:
- ✅ Lógica pura (bits, operadores, estruturas de controle, tipos, funções): roda no Jobe, desde que você troque o
Arduino.hreal por um mock. - ❌ Firmware real (WiFi, GPIO físico, timing, periféricos): não roda no Jobe — isso é
pio run(compila para ESP32) + Wokwi (executa).
Compile o código do aluno contra um Arduino.h falso que implementa as funções em C puro (as macros de bit, um Serial que escreve no stdout), rode no Jobe e compare o stdout com o esperado.
Três níveis do que se pode testar
- A · Função pura
- B · Sketch com Serial
- C · Firmware real
O aluno escreve uma função (ex.: ehPar, menorDivisor, somaDigitos). O harness chama a função e imprime o resultado.
// aluno
bool ehPar(uint8_t n) { return (n & 1) == 0; }
// harness (lê n do stdin, imprime 0/1)
int main() { int n; scanf("%d", &n); printf("%d\n", ehPar((uint8_t)n) ? 1 : 0); return 0; }
Testável e auto-corrigível por stdout — é o caso mais comum.
O aluno escreve um sketch (setup()/loop()) que imprime via Serial. O mock manda o Serial para o stdout e um driver chama setup().
// aluno (trecho do lab de bits)
void setup() {
uint8_t reg = 0; bitSet(reg,0); bitSet(reg,7);
Serial.print("reg=0x"); Serial.println(reg, HEX);
}
void loop() {}
// driver: emula o core do Arduino
int main() { setup(); return 0; } // para o loop: for(int i=0;i<5;i++) loop();
Saída capturada: reg=0x81. Funciona para a parte de saída serial.
WiFi, digitalRead de um botão real, millis() para temporização, MQTT… não dá para executar no Jobe (não há hardware nem a arquitetura do ESP32).
- Compila? →
pio run(cross-compila para o ESP32). É o checkcompilado autograder. - Comporta-se certo? → Wokwi (simulação) ou ESP32 físico.
A receita (nível A/B)
- Mock
Arduino.h— um header que implementa em C puro: macros de bit (bit,bitRead,bitSet,bitClear,bitWrite,highByte,lowByte), constantes (HIGH,LOW,INPUT_PULLUP,LED_BUILTIN,HEX…), GPIO como no-op,millis/delaysimulados e umSerialque escreve nostdout. - Combinar em um único fonte (o Jobe compila só o
sourcecode): mock + código do aluno (removendo o#include <Arduino.h>dele) + harness. - Enviar ao Jobe (
POST .../restapi/runs,language_id: "cpp",input= stdin do caso). - Avaliar: passou se
outcome == 15e ostdoutbate com o esperado. Seoutcome == 11, mostre ocmpinfo(erro de compilação).
Ferramentas prontas
Este guia acompanha uma pasta jobe-arduino/:
jobe-arduino/
├── arduino-mock/Arduino.h # o mock (bit macros, Serial->stdout, GPIO no-op...)
├── harness/harness_funcao.cpp # nível A: chama a funcao lendo o stdin
├── harness/driver_sketch.cpp # nível B: chama setup() uma vez
├── jobe_test.py # driver: monta o fonte, roda no Jobe (ou g++), corrige
├── sol.cpp # exemplo de resposta do aluno
└── tests.json # casos: [{input, expected}]
O jobe_test.py tem um modo --local que usa o g++ da sua máquina em vez do Jobe — ideal para desenvolver os testes offline (o resultado é o mesmo que o Jobe daria).
- Testar local (g++)
- Testar via Jobe
python jobe_test.py --local \
--mock arduino-mock/Arduino.h \
--student sol.cpp \
--harness harness/harness_funcao.cpp \
--tests tests.json
python jobe_test.py --jobe http://localhost:4000 \
--mock arduino-mock/Arduino.h \
--student sol.cpp \
--harness harness/harness_funcao.cpp \
--tests tests.json
# --apikey SUA-CHAVE (se o Jobe exigir)
Saída típica:
[1] OK
[2] OK
[3] OK
3/3 casos passaram.
Aluno com erro mostra esperado vs obtido; aluno que não compila mostra a mensagem do compilador. O código de saída é 0 só quando todos os casos passam — pronto para CI.
Integração com o autograder do curso
Isto encaixa como um novo tipo de check no grade.py: em vez de só contem/compila, um check jobe que executa o código do aluno e confere a saída.
# esboço de check "jobe" (roda no estado do commit da tarefa)
{"tipo": "jobe", "arquivo": "src/main.cpp",
"harness": "harness/harness_funcao.cpp",
"casos": [{"input": "4\n", "expected": "1\n"},
{"input": "7\n", "expected": "0\n"}],
"desc": "ehPar passa nos casos"}
O check monta o fonte (mock + arquivo do aluno + harness) e chama o Jobe; passa se todos os casos derem outcome 15 e a saída certa. Assim você avalia comportamento, não só presença de texto — sem instalar compiladores no runner de CI (o Jobe faz isso).
Limitações honestas do mock
- O mock não simula hardware:
digitalReaddevolve um valor fixo,millis()é um contador falso,delay()não espera. Bom para lógica e saída serial; inútil para timing real, interrupções, WiFi, I2C/SPI. - Cubra no mock apenas o que os exercícios usam. Se um lab usa uma função nova do Arduino, adicione-a ao
Arduino.h. - Diferenças de tamanho de tipo: o host geralmente é 64 bits; se um exercício depende de
intde 32 bits (como no ESP32), fixe os tipos no teste (int32_t,uint8_t) para o resultado bater.
Para tudo que precisa de hardware de verdade, o caminho continua sendo pio run + Wokwi. Veja também a API REST do Jobe.
Guia — testar código Arduino no Jobe.
jobe-arduino
Essa é a pergunta certa e tem uma resposta honesta com uma fronteira clara. O ponto central: o Jobe executa no compilador do host (g++), não no ESP32 — então firmware de verdade (WiFi, pinos, timing) não roda lá. Mas boa parte do que os labs avaliam é lógica pura em C/C++ (bits, estruturas de controle, tipos), e isso dá para rodar no Jobe — desde que você substitua o Arduino.h por um mock.
Deixa eu validar a técnica de verdade compilando com g++ aqui (é exatamente o que o Jobe faria), antes de te entregar o mock + harness + driver.Tier A funciona. Agora o Tier B: um sketch completo com setup()/Serial, compilado com o mock + um main() driver que chama setup() — captura o Serial como stdout.Ambos os tiers funcionam. Um detalhe do Jobe: ele compila um arquivo sourcecode (os arquivos de apoio não são linkados automaticamente). Então o padrão é montar um fonte que faz #include do código do aluno. Vou validar esse formato de translation-unit único para o Tier B (é como o Jobe realmente compila).Validado o padrão do Jobe (um sourcecode que inclui o mock + código do aluno) e o caso de erro de compilação (vai para cmpinfo, outcome 11). Agora entrego o mock, um driver Python (com modo --local via g++ para você testar offline — e para eu validar aqui) e o guia. Primeiro o mock e o driver.Agora testo o driver de ponta a ponta com --local (que replica o que o Jobe faz), incluindo o caso de aluno com #include <Arduino.h> (o driver remove e injeta o mock):O driver funciona de ponta a ponta: aluno correto → 3/3 (exit 0); com bug → mostra esperado vs. obtido (exit 1); sem compilar → erro de compilação (o que iria no cmpinfo/outcome 11 no Jobe). Vou limpar os arquivos de teste e escrever o guia.Tudo validado (MDX limpo, driver compila).
Resposta direta
O Jobe roda no g++ do host, não no ESP32 — então a estratégia depende do que é o código:
- Lógica pura (bits, operadores, controle, tipos, funções): dá para rodar no Jobe, trocando o
Arduino.hreal por um mock que implementa as macros de bit e umSerialque escreve nostdout. - Firmware real (WiFi, GPIO, timing, MQTT): não roda no Jobe — isso é
pio run(compila) + Wokwi (executa).
A receita (validada de verdade)
Testei o fluxo inteiro compilando com g++ aqui — exatamente o que o Jobe faz:
- Mock
Arduino.h—bit/bitRead/bitSet/highByte/lowByte, constantes (HIGH,LED_BUILTIN,HEX…), GPIO no-op,millis/delayfalsos eSerial→stdout. - Um único fonte = mock + código do aluno (o driver remove o
#include <Arduino.h>dele) + harness. É assim que o Jobe compila (umsourcecodesó). - POST para
/restapi/runscomlanguage_id: "cpp"einput(stdin do caso). - Passou se
outcome == 15e ostdoutbate; seoutcome == 11, mostra ocmpinfo.
Provei os três cenários no jobe_test.py --local: aluno correto → 3/3; com bug → mostra esperado vs. obtido; sem compilar → erro de compilação. O include <Arduino.h> do aluno é removido e o mock injetado automaticamente.
O que entreguei em jobe-arduino/
arduino-mock/Arduino.h— o mock (testado com o lab de bits:reg=0x81,hi=AB/lo=CD).jobe_test.py— driver que monta o fonte, roda no Jobe (--jobe URL) ou no g++ local (--local, para desenvolver sem Jobe), corrige porstdoute sai com código ≠ 0 se algum caso falhar (pronto para CI). Usa sóurllib(sem dependências).harness/harness_funcao.cpp(nível A) eharness/driver_sketch.cpp(nível B),sol.cppetests.jsonde exemplo.
Integração e limites
No guia mostro como isso vira um check jobe no seu grade.py (executa o código do aluno no estado do commit e confere a saída — avalia comportamento, não só presença de texto, sem instalar compiladores no runner). E deixo os limites honestos: o mock não simula hardware (digitalRead fixo, millis falso), serve para lógica e saída serial; e cuidado com tamanho de tipos (o host é 64 bits — fixe int32_t/uint8_t quando o exercício depender do tamanho do ESP32).
Se quiser, o próximo passo é eu implementar o check jobe no grade.py (com a lógica já testada aqui) e adicionar o jobeinabox como serviço no seu docker-compose.yml, para o Jobe subir junto da stack e o autograder usá-lo.
O mock (bit macros, Serial->stdout, GPIO no-op...) arduino-mock/Arduino.h
#ifndef ARDUINO_MOCK_H
#define ARDUINO_MOCK_H
#include <cstdint>
#include <cstdio>
#include <string>
typedef uint8_t byte;
typedef bool boolean;
#define HIGH 1
#define LOW 0
#define INPUT 0
#define OUTPUT 1
#define INPUT_PULLUP 2
#define LED_BUILTIN 2
#define DEC 10
#define HEX 16
#define OCT 8
#define BIN 2
#define bit(b) (1UL << (b))
#define bitRead(v,b) (((v) >> (b)) & 0x01)
#define bitSet(v,b) ((v) |= (1UL << (b)))
#define bitClear(v,b) ((v) &= ~(1UL << (b)))
#define bitWrite(v,b,x) ((x) ? bitSet(v,b) : bitClear(v,b))
#define highByte(w) ((uint8_t)((w) >> 8))
#define lowByte(w) ((uint8_t)((w) & 0xff))
inline void pinMode(int,int){}
inline void digitalWrite(int,int){}
inline int digitalRead(int){return HIGH;}
inline unsigned long millis(){static unsigned long t=0;return t+=1;}
inline void delay(unsigned long){}
struct SerialMock {
void begin(long){}
void print(const char*s){fputs(s,stdout);}
void print(char c){putchar(c);}
void print(int v,int base=DEC){pb((long)v,base);}
void print(long v,int base=DEC){pb(v,base);}
void print(unsigned int v,int base=DEC){pb((long)v,base);}
void print(unsigned long v,int base=DEC){pb((long)v,base);}
void print(double v){printf("%.2f",v);}
void print(double v,int d){printf("%.*f",d,v);}
void println(){putchar('\n');}
void println(const char*s){fputs(s,stdout);putchar('\n');}
template<class T> void println(T v){print(v);putchar('\n');}
template<class T> void println(T v,int base){print(v,base);putchar('\n');}
private:
void pb(long v,int base){
if(base==HEX){printf("%lX",(unsigned long)v);}
else if(base==OCT){printf("%lo",(unsigned long)v);}
else if(base==BIN){unsigned long u=(unsigned long)v; if(!u){putchar('0');return;} char b[65];int i=0; while(u){b[i++]='0'+(u&1);u>>=1;} while(i--)putchar(b[i]);}
else printf("%ld",v);
}
};
static SerialMock Serial;
#endif
Nível A: chama a funcao lendo o stdin harness/harness_funcao.cpp
// Harness Tier A: testa UMA funcao do aluno lendo a entrada do stdin.
// Ajuste conforme a assinatura da funcao avaliada.
int main() {
int n;
if (scanf("%d", &n) != 1) return 1;
printf("%d\n", ehPar((uint8_t)n) ? 1 : 0);
return 0;
}
Nível B: chama setup() uma vez harness/driver_sketch.cpp
// Harness Tier B: emula o "core" do Arduino chamando setup() uma vez.
// Para exercitar o loop(), troque por: setup(); for(int i=0;i<5;i++) loop();
int main() {
setup();
return 0;
}
Driver: monta o fonte, roda no Jobe (ou g++), corrige jobe_test.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
jobe_test.py — testa codigo estilo Arduino (PlatformIO) SEM Moodle.
Monta um unico fonte C++ = mock Arduino.h + codigo do aluno (sem o include de
Arduino) + harness, e o executa:
- no sandbox Jobe (POST /jobe/index.php/restapi/runs), ou
- localmente com g++ (--local), util para desenvolver sem um Jobe no ar.
Cada caso de teste fornece a entrada (stdin) e a saida esperada (stdout).
A funcao/sketch passa quando outcome == 15 (OK) e o stdout bate.
Uso:
# local (usa g++; nao precisa de Jobe)
python jobe_test.py --local --mock arduino-mock/Arduino.h \\
--student sol.cpp --harness harness/harness_funcao.cpp --tests tests.json
# via Jobe
python jobe_test.py --jobe http://localhost:4000 --mock arduino-mock/Arduino.h \\
--student sol.cpp --harness harness/harness_funcao.cpp --tests tests.json
tests.json:
[ {"input": "4\\n", "expected": "1\\n"},
{"input": "7\\n", "expected": "0\\n"} ]
"""
import argparse
import json
import re
import subprocess
import sys
import tempfile
import os
import urllib.request
import urllib.error
from pathlib import Path
OK = 15 # outcome de sucesso do Jobe
# Remove os includes do Arduino (o mock ja provê tudo em um unico fonte).
INCLUDE_ARDUINO = re.compile(r'^\s*#\s*include\s*[<"]\s*Arduino\.h\s*[>"]\s*$', re.M)
def build_source(mock_path, student_path, harness_path):
mock = Path(mock_path).read_text(encoding="utf-8")
aluno = Path(student_path).read_text(encoding="utf-8")
aluno = INCLUDE_ARDUINO.sub("// (include de Arduino.h removido pelo mock)", aluno)
harness = Path(harness_path).read_text(encoding="utf-8")
return f"{mock}\n// ===== codigo do aluno =====\n{aluno}\n// ===== harness =====\n{harness}\n"
# ---------- execução via Jobe ----------
def run_jobe(jobe_url, source, stdin, apikey=None, language="cpp"):
url = jobe_url.rstrip("/") + "/jobe/index.php/restapi/runs"
body = json.dumps({"run_spec": {
"language_id": language,
"sourcecode": source,
"input": stdin,
}}).encode("utf-8")
headers = {"Content-Type": "application/json"}
if apikey:
headers["X-API-KEY"] = apikey
req = urllib.request.Request(url, data=body, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=30) as resp:
r = json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
return {"outcome": -1, "stdout": "", "cmpinfo": "", "stderr": f"HTTP {e.code}: {e.read().decode('utf-8', 'replace')}"}
except Exception as e: # noqa: BLE001
return {"outcome": -1, "stdout": "", "cmpinfo": "", "stderr": str(e)}
return {"outcome": r.get("outcome"), "stdout": r.get("stdout", ""),
"cmpinfo": r.get("cmpinfo", ""), "stderr": r.get("stderr", "")}
# ---------- execução local (g++), para dev sem Jobe ----------
def run_local(source, stdin):
with tempfile.TemporaryDirectory() as d:
src = Path(d) / "prog.cpp"
exe = Path(d) / "prog"
src.write_text(source, encoding="utf-8")
comp = subprocess.run(["g++", "-std=c++17", str(src), "-o", str(exe)],
capture_output=True, text=True)
if comp.returncode != 0:
return {"outcome": 11, "stdout": "", "cmpinfo": comp.stderr, "stderr": ""}
try:
run = subprocess.run([str(exe)], input=stdin, capture_output=True,
text=True, timeout=10)
except subprocess.TimeoutExpired:
return {"outcome": 13, "stdout": "", "cmpinfo": "", "stderr": "timeout"}
outcome = OK if run.returncode == 0 else 12
return {"outcome": outcome, "stdout": run.stdout, "cmpinfo": "", "stderr": run.stderr}
def norm(s):
# comparacao tolerante a espacos no fim das linhas e newline final
return "\n".join(line.rstrip() for line in (s or "").rstrip("\n").split("\n"))
def main(argv=None):
ap = argparse.ArgumentParser(description="Testa codigo Arduino via Jobe (ou g++ local).")
ap.add_argument("--mock", required=True, help="caminho do Arduino.h (mock)")
ap.add_argument("--student", required=True, help="codigo do aluno (.cpp)")
ap.add_argument("--harness", required=True, help="harness/driver (.cpp)")
ap.add_argument("--tests", required=True, help="tests.json [{input, expected}]")
ap.add_argument("--jobe", help="URL do Jobe (ex.: http://localhost:4000)")
ap.add_argument("--apikey", help="X-API-KEY do Jobe, se exigido")
ap.add_argument("--local", action="store_true", help="executa com g++ local em vez do Jobe")
args = ap.parse_args(argv)
if not args.local and not args.jobe:
ap.error("informe --jobe URL ou use --local")
source = build_source(args.mock, args.student, args.harness)
casos = json.loads(Path(args.tests).read_text(encoding="utf-8"))
passou = 0
for i, caso in enumerate(casos, 1):
entrada = caso.get("input", "")
esperado = caso.get("expected", "")
res = run_local(source, entrada) if args.local else run_jobe(args.jobe, source, entrada, args.apikey)
if res["outcome"] == 11:
print(f"[{i}] ERRO DE COMPILACAO\n{res['cmpinfo'].strip()}")
continue
if res["outcome"] != OK:
print(f"[{i}] FALHOU (outcome={res['outcome']}) stderr={res['stderr'].strip()}")
continue
if norm(res["stdout"]) == norm(esperado):
passou += 1
print(f"[{i}] OK")
else:
print(f"[{i}] SAIDA DIFERENTE\n esperado: {esperado!r}\n obtido: {res['stdout']!r}")
total = len(casos)
print(f"\n{passou}/{total} casos passaram.")
return 0 if passou == total else 1
if __name__ == "__main__":
raise SystemExit(main())
Exemplo de resposta do aluno sol.cpp
#include <Arduino.h>
bool ehPar(uint8_t n) {
return (n & 1) == 0;
}
casos: tests.json [{input, expected}]
[
{ "input": "4\n", "expected": "1\n" },
{ "input": "7\n", "expected": "0\n" },
{ "input": "0\n", "expected": "1\n" }
]