Compare commits
107 Commits
04be62e0da
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
b16dc63b9b
|
|||
|
330fa87e1a
|
|||
|
d0e4b52f97
|
|||
|
227eddcb1c
|
|||
|
a6013efa9e
|
|||
|
0eda8596a6
|
|||
|
dd24eba69a
|
|||
|
adf8bcb252
|
|||
|
5a4ab27fc5
|
|||
|
d1208fa8b0
|
|||
|
9ac071764e
|
|||
|
2ff6da13b3
|
|||
|
6d489da150
|
|||
|
c60ae08f56
|
|||
|
13bab63618
|
|||
|
5b768f6a21
|
|||
|
cd4fc02f44
|
|||
|
8333e1777f
|
|||
|
6398b0eec8
|
|||
|
3ef5b138f3
|
|||
|
a5974b8b30
|
|||
|
ccc5a51a2d
|
|||
|
8b3dd02cac
|
|||
|
f9aa659ee3
|
|||
|
6c4b827c36
|
|||
|
a9d71de4b1
|
|||
|
97f1d69bcc
|
|||
|
096be9ed68
|
|||
|
fcba598a52
|
|||
|
d401326ae3
|
|||
|
c92eb1e7af
|
|||
|
18381cea6b
|
|||
|
3caa628d59
|
|||
|
de95910e70
|
|||
|
8b13a22397
|
|||
|
cd09974c2a
|
|||
|
8ddd48508a
|
|||
|
a94e7f6142
|
|||
|
83bf313305
|
|||
|
1a1558cfad
|
|||
|
0fa5da224d
|
|||
|
a1a4eeaed7
|
|||
|
72f7833c6c
|
|||
|
321bc07eb1
|
|||
|
cbd16b3f60
|
|||
|
4a1c67022d
|
|||
|
ca989b6943
|
|||
|
a7d29b7cce
|
|||
|
b434eb3a49
|
|||
|
0f264ef603
|
|||
|
e28da5def2
|
|||
|
86572f06e9
|
|||
|
167297657a
|
|||
|
8cd7b3e6e0
|
|||
|
35753fe863
|
|||
|
e043450457
|
|||
|
490ea5edf4
|
|||
|
f2d6982a79
|
|||
|
5e45458ca2
|
|||
|
4dbb440a76
|
|||
|
b6167dfc5e
|
|||
|
ef4c4dcf17
|
|||
|
0a0feb93b5
|
|||
|
ad320dc703
|
|||
|
360b5fa255
|
|||
|
615e3f1291
|
|||
|
c4662f5001
|
|||
|
0b83eff6f8
|
|||
|
09ec96a282
|
|||
|
2e323d7a06
|
|||
|
359a7a705f
|
|||
|
c4bd3fc6a8
|
|||
|
5e16637b52
|
|||
|
7951282376
|
|||
|
f9c404b69e
|
|||
|
dd9a027c5a
|
|||
|
c5cd007c8f
|
|||
|
02fc080bf8
|
|||
|
0167d324b0
|
|||
|
3604e0a889
|
|||
|
6e01777678
|
|||
|
eaed337fc1
|
|||
|
f1fb32f44c
|
|||
|
8c5e5faf7a
|
|||
|
861f0f6d51
|
|||
|
4689d69a37
|
|||
|
5032273796
|
|||
|
49c37548c0
|
|||
|
f8350bfdaf
|
|||
|
c4856d9948
|
|||
|
2e9c38f71b
|
|||
|
74fbe2676e
|
|||
|
1070b1e1fb
|
|||
|
e6c971f371
|
|||
|
ee22ee7480
|
|||
|
b4110d6abd
|
|||
|
e4a71ade59
|
|||
|
2f0e2a40a6
|
|||
|
375787aa60
|
|||
|
8a3078ad5c
|
|||
|
803ec7ca9c
|
|||
|
93554683c7
|
|||
|
3577082908
|
|||
|
ec97f27c61
|
|||
|
69a9183c7f
|
|||
|
f4ffd20fcb
|
|||
|
286b1d6d43
|
@@ -35,6 +35,8 @@ jobs:
|
|||||||
run: mypy src/
|
run: mypy src/
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
|
env:
|
||||||
|
DATABASE_URL: "sqlite+aiosqlite:///:memory:"
|
||||||
run: pytest --cov=src --cov-report=term-missing --cov-fail-under=80
|
run: pytest --cov=src --cov-report=term-missing --cov-fail-under=80
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
@@ -61,7 +63,7 @@ jobs:
|
|||||||
run: npm run lint
|
run: npm run lint
|
||||||
|
|
||||||
- name: Type check
|
- name: Type check
|
||||||
run: tsc --noEmit
|
run: npx tsc --noEmit
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: npm test
|
run: npm test
|
||||||
@@ -86,8 +88,6 @@ jobs:
|
|||||||
context: ./backend
|
context: ./backend
|
||||||
push: false
|
push: false
|
||||||
tags: codetutor-backend:test
|
tags: codetutor-backend:test
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
|
|
||||||
- name: Build frontend image
|
- name: Build frontend image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
@@ -95,5 +95,3 @@ jobs:
|
|||||||
context: ./frontend
|
context: ./frontend
|
||||||
push: false
|
push: false
|
||||||
tags: codetutor-frontend:test
|
tags: codetutor-frontend:test
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
|
|||||||
@@ -177,3 +177,92 @@ async def test_get_stats(client: AsyncClient, db_session: AsyncSession) -> None:
|
|||||||
assert data["by_difficulty"]["easy"] == 1
|
assert data["by_difficulty"]["easy"] == 1
|
||||||
assert len(data["by_category"]) == 1
|
assert len(data["by_category"]) == 1
|
||||||
assert len(data["by_pattern"]) == 1
|
assert len(data["by_pattern"]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_pattern_tutorial(client: AsyncClient, db_session: AsyncSession) -> None:
|
||||||
|
await create_sample_data(db_session)
|
||||||
|
response = await client.get("/api/patterns/two-pointers/tutorial")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["name"] == "Two Pointers"
|
||||||
|
assert data["learning_progression"]["warmup"][0]["title"] == "Two Sum"
|
||||||
|
assert len(data["learning_progression"]["core"]) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_pattern_tutorial_not_found(client: AsyncClient) -> None:
|
||||||
|
response = await client.get("/api/patterns/nonexistent/tutorial")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
async def test_submit_passing(client: AsyncClient, db_session: AsyncSession) -> None:
|
||||||
|
data = await create_sample_data(db_session)
|
||||||
|
q = data["question"]
|
||||||
|
q.test_cases = {
|
||||||
|
"visible": [{"input": {"nums": [2, 7], "target": 9}, "expected": [0, 1]}],
|
||||||
|
"hidden": [{"input": {"nums": [3, 2, 4], "target": 6}, "expected": [1, 2]}],
|
||||||
|
}
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
response = await client.post(
|
||||||
|
"/api/questions/two-sum/submit",
|
||||||
|
json={
|
||||||
|
"code": "def solve(nums, t): pass",
|
||||||
|
"hidden_outputs": [{"test_id": 1, "output": [1, 2]}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
result = response.json()
|
||||||
|
assert result["passed"] is True
|
||||||
|
assert result["total_passed"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_submit_wrong_output(client: AsyncClient, db_session: AsyncSession) -> None:
|
||||||
|
data = await create_sample_data(db_session)
|
||||||
|
data["question"].test_cases = {
|
||||||
|
"visible": [],
|
||||||
|
"hidden": [{"input": {"nums": [1, 2], "target": 3}, "expected": [0, 1]}],
|
||||||
|
}
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
response = await client.post(
|
||||||
|
"/api/questions/two-sum/submit",
|
||||||
|
json={
|
||||||
|
"code": "def solve(nums, t): pass",
|
||||||
|
"hidden_outputs": [{"test_id": 0, "output": [9, 9]}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["passed"] is False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_submit_missing_output(client: AsyncClient, db_session: AsyncSession) -> None:
|
||||||
|
data = await create_sample_data(db_session)
|
||||||
|
data["question"].test_cases = {
|
||||||
|
"visible": [],
|
||||||
|
"hidden": [{"input": {"nums": [1, 2], "target": 3}, "expected": [0, 1]}],
|
||||||
|
}
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
response = await client.post(
|
||||||
|
"/api/questions/two-sum/submit",
|
||||||
|
json={"code": "def solve(nums, t): pass", "hidden_outputs": []},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["hidden_results"][0]["error"] == "No output provided"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_submit_no_test_cases(client: AsyncClient, db_session: AsyncSession) -> None:
|
||||||
|
await create_sample_data(db_session)
|
||||||
|
response = await client.post(
|
||||||
|
"/api/questions/two-sum/submit",
|
||||||
|
json={"code": "pass", "hidden_outputs": []},
|
||||||
|
)
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
async def test_submit_not_found(client: AsyncClient) -> None:
|
||||||
|
response = await client.post(
|
||||||
|
"/api/questions/nonexistent/submit",
|
||||||
|
json={"code": "pass", "hidden_outputs": []},
|
||||||
|
)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|||||||
@@ -215,6 +215,55 @@ class TestPatternService:
|
|||||||
|
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
|
async def test_get_tutorial(self, db_session: AsyncSession) -> None:
|
||||||
|
related = Pattern(name="BFS", slug="bfs", description="Breadth-first search")
|
||||||
|
pattern = Pattern(
|
||||||
|
name="Two Pointers",
|
||||||
|
slug="two-pointers",
|
||||||
|
description="Two pointer technique",
|
||||||
|
common_mistakes=[
|
||||||
|
{"title": "Off by one", "description": "Wrong bounds", "fix": "Use <="}
|
||||||
|
],
|
||||||
|
variations=[{"name": "Fast/slow", "description": "Different speeds"}],
|
||||||
|
related_patterns=["bfs"],
|
||||||
|
visualization_examples=[{"id": "ex1", "title": "Basic", "code": "x=1", "steps": []}],
|
||||||
|
)
|
||||||
|
db_session.add_all([related, pattern])
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
q1 = Question(
|
||||||
|
title="Easy Q", slug="easy-q", difficulty=Difficulty.EASY, description="Easy."
|
||||||
|
)
|
||||||
|
q2 = Question(title="Med Q", slug="med-q", difficulty=Difficulty.MEDIUM, description="Med.")
|
||||||
|
q3 = Question(
|
||||||
|
title="Hard Q", slug="hard-q", difficulty=Difficulty.HARD, description="Hard."
|
||||||
|
)
|
||||||
|
q1.patterns = [pattern]
|
||||||
|
q2.patterns = [pattern]
|
||||||
|
q3.patterns = [pattern]
|
||||||
|
db_session.add_all([q1, q2, q3])
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
service = PatternService(db_session)
|
||||||
|
result = await service.get_pattern_tutorial("two-pointers")
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert result.name == "Two Pointers"
|
||||||
|
assert len(result.common_mistakes) == 1
|
||||||
|
assert len(result.variations) == 1
|
||||||
|
assert len(result.related_patterns) == 1
|
||||||
|
assert result.related_patterns[0].slug == "bfs"
|
||||||
|
assert len(result.learning_progression.warmup) == 1
|
||||||
|
assert len(result.learning_progression.core) == 1
|
||||||
|
assert len(result.learning_progression.challenge) == 1
|
||||||
|
assert len(result.visualization_examples) == 1
|
||||||
|
|
||||||
|
async def test_get_tutorial_not_found(self, db_session: AsyncSession) -> None:
|
||||||
|
service = PatternService(db_session)
|
||||||
|
result = await service.get_pattern_tutorial("nonexistent")
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
class TestStatsService:
|
class TestStatsService:
|
||||||
async def test_get_stats(self, db_session: AsyncSession) -> None:
|
async def test_get_stats(self, db_session: AsyncSession) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user