Chapter 5 — Building Tool 2: Maven Build & Test Runner
ทำไมต้องชื่อนี้และเรียนไปเพื่ออะไร?
ใน Day 4 เราติด "ดวงตา (File Reader)" ให้ Agent สามารถเปิดส่องดูโค้ดบนเครื่องได้แล้ว แต่ถ้า Agent ทำได้แค่อ่านโค้ด มันก็ยังเป็นได้แค่ "คนดูที่วิจารณ์แต่ลงมือทำอะไรไม่ได้"
Day 5 คือก้าวสำคัญในการติด "มือและเท้าสำหรับการปฏิบัติการ (Actionable Execution Layer)" ผ่านเครื่องมือตัวที่ 2: Maven Build & Test Runner Tool ซึ่งจะเชื่อมต่อ Python เข้ากับ Process ของระบบปฏิบัติการผ่าน subprocess.run() เพื่อสั่งคอมไพล์โค้ดจริง รันชุดทดสอบ Unit Tests จริง และดักจับ Stack Trace จาก Log ย้อนกลับมาให้โมเดลอ่าน
หัวใจสำคัญระดับ Senior ไม่ใช่แค่การรันคำสั่ง แต่คือ "Process Sandboxing & Resource Isolation": การตั้งเวลาตัดจบ (timeout) เพื่อกัน infinite loops, การเช็ค Return Code (`0` = ผ่าน, `!= 0` = บั๊ก), และการสกัด Error Log Slicing เพื่อไม่ให้ Log ยาวพันบรรทัดระเบิด Context Window ของ Mac M3!
โค้ดประจำ Day 5 จะต่อยอดโครงสร้าง se_agent/ โดยเพิ่มโมดูล core/tools/test_runner.py, โฟลเดอร์จำลองโปรเจกต์ Java sample_maven_project/, และสคริปต์ test_maven_runner.py:
ทำหน้าที่ยิงคำสั่ง Terminal mvn test ผ่าน subprocess.run() ควบคุม timeout 120 วินาที ตรวจสอบ Exit code และดักจับ stdout/stderr ตัดทอนให้เหลือเฉพาะส่วน Failure Summary เพื่อส่งต่อให้ AI วิเคราะห์
สคริปต์รันบน Terminal เพื่อทดสอบส่งโจทย์: "ช่วยรันเทสใน sample_maven_project ให้หน่อยว่าผ่านไหม ถ้าไม่ผ่านให้บอกว่าพังตรงไหน" AI จะตัดสินใจเรียก Tool รันเทสจริง ➔ ได้ผล Error Log ➔ วิเคราะห์สาเหตุให้ฟัง
ถ้าใช้ os.system("mvn test") เราจะไม่สามารถดักจับ output เข้าตัวแปรได้, ถ้าคำสั่งค้างหรือติด loop จะไม่มี timeout มาตัดจบ, และเสี่ยงต่อ Command Injection หากส่ง string ต่อกันตรงๆ
ส่งคำสั่งเป็น List ["mvn", "test"] ป้องกัน Shell Injection, แยก stdout/stderr ชัดเจน, กำหนด timeout ชัดเจน และสกัดเฉพาะ Error Summary ส่งกลับให้โมเดลประหยัด Token สูงสุด
import subprocess
import shutil
from pathlib import Path
from typing import Optional, Dict, Any
class BuildExecutionError(Exception):
"""Custom exception สำหรับข้อผิดพลาดที่ไม่สามารถรัน Build Process ได้"""
pass
def run_maven_test(project_dir: str = ".", test_name: Optional[str] = None, timeout_seconds: int = 120) -> str:
"""
เครื่องมือสำหรับสั่งรัน Unit Tests ของโปรเจกต์ Java ผ่าน Maven
:param project_dir: ไดเรกทอรีของโปรเจกต์ที่มีไฟล์ pom.xml
:param test_name: (ทางเลือก) ระบุชื่อคลาสเทสเฉพาะเจาะจง เช่น 'CalculatorTest'
:param timeout_seconds: เวลาสูงสุดที่อนุญาตให้คำสั่งรัน ป้องกันการติด Infinite Loop
:return: รายงานผลลัพธ์การรันเทส (BUILD SUCCESS หรือ BUILD FAILURE พร้อม Stack Trace)
"""
# 1. แปลง project_dir เป็น Absolute Path และตรวจสอบไฟล์ pom.xml
target_path = Path(project_dir).resolve()
pom_file = target_path / "pom.xml"
if not pom_file.exists():
raise BuildExecutionError(f"ไม่พบไฟล์ 'pom.xml' ในโฟลเดอร์: {target_path}")
# 2. ตรวจสอบว่าในเครื่อง Mac มีคำสั่ง mvn ติดตั้งอยู่หรือไม่
mvn_bin = shutil.which("mvn")
if not mvn_bin:
# Fallback: ตรวจสอบว่ามี Maven Wrapper (mvnw) อยู่ในโปรเจกต์หรือไม่
mvnw = target_path / "mvnw"
if mvnw.exists():
mvn_cmd = [str(mvnw)]
else:
raise BuildExecutionError("ไม่พบคำสั่ง 'mvn' หรือ 'mvnw' ในระบบ โปรดติดตั้ง Maven ก่อนใช้งาน")
else:
mvn_cmd = [mvn_bin]
# 3. จัดเตรียมคำสั่ง Command ในรูปแบบ List
# -B: Batch mode (ไม่แสดงสีหรือข้อความ interactive)
# -Dtest=... : ระบุชื่อเทสเจาะจงถ้ามี
cmd = mvn_cmd + ["test", "-B"]
if test_name:
cmd.append(f"-Dtest={test_name}")
try:
# 4. ยิง Process ผ่าน subprocess.run
process = subprocess.run(
cmd,
cwd=str(target_path),
capture_output=True,
text=True,
timeout=timeout_seconds,
encoding="utf-8",
errors="replace"
)
stdout = process.stdout or ""
stderr = process.stderr or ""
full_output = stdout + "\n" + stderr
# 5. วิเคราะห์ Exit Code (0 = สำเร็จ, ไม่ใช่ 0 = ล้มเหลว)
if process.returncode == 0:
return (
"✅ BUILD SUCCESS: การรันชุดทดสอบทั้งหมดผ่านเรียบร้อย 100%!\n"
f"รายละเอียดผลลัพธ์:\n{_extract_summary_lines(stdout, 15)}"
)
else:
# สกัดเฉพาะบรรทัดล่างสุดที่สรุป Failure เพื่อไม่ให้กิน Context Window
failure_summary = _extract_summary_lines(full_output, 35)
return (
f"❌ BUILD FAILURE: ตรวจพบข้อผิดพลาดในชุดทดสอบ (Exit Code {process.returncode})\n"
"--------------------------------------------------\n"
f"{failure_summary}\n"
"--------------------------------------------------"
)
except subprocess.TimeoutExpired:
raise BuildExecutionError(f"Timeout Error: คำสั่งรันเทสใช้เวลานานเกินกว่า {timeout_seconds} วินาที")
except Exception as err:
raise BuildExecutionError(f"เกิดข้อผิดพลาดขณะรันกระบวนการ Maven: {str(err)}")
def _extract_summary_lines(output: str, max_lines: int = 30) -> str:
"""ฟังก์ชันช่วยตัดทอน Log ยาวๆ ให้เหลือเฉพาะส่วนท้ายที่สำคัญ"""
lines = [line.strip() for line in output.splitlines() if line.strip()]
if len(lines) <= max_lines:
return "\n".join(lines)
return "[...ตัดทอนบันทึกตอนต้นออก...]\n" + "\n".join(lines[-max_lines:])
ชำแหละไวยากรณ์สำคัญใน core/tools/test_runner.py
shutil.which("mvn")
ในภาษา Python คำสั่ง shutil.which() ใช้สแกนหาตำแหน่ง binary executable บน PATH ของระบบ เช่น คืนค่า /opt/homebrew/bin/mvn ช่วยตรวจสอบก่อนรันว่ามีโปรแกรมจริงไหม
subprocess.run(cmd, cwd=str(target_path), capture_output=True, timeout=120)
cwd สั่งให้ process ย้ายไปทำงานในโฟลเดอร์โปรเจกต์เป้าหมาย, capture_output=True ดักทั้ง stdout และ stderr เข้าตัวแปรในหน่วยความจำ, และ timeout ตัดจบหากเกิด deadlocks
process.returncode == 0
Exit Code ของคำสั่งระดับ OS สากล หากส่งคืนเลข 0 แปลว่าทุก test case สำเร็จสมบูรณ์ หากเป็นเลขอื่น (เช่น 1) แปลว่ามี assertion failure หรือ compile error
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>sample-maven-project</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
// 3.1: src/main/java/Calculator.java (มีบั๊ก: คำนวณภาษีผิดตัวเลข)
public class Calculator {
public int multiply(int a, int b) {
return a * b;
}
public double applyDiscount(double price, double discount) {
// บั๊กจำลอง: เขียนผิดเป็นลบเงินสองเท่า
return price - (discount * 2.0);
}
}
// 3.2: src/test/java/CalculatorTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CalculatorTest {
@Test
void testMultiply() {
Calculator calc = new Calculator();
assertEquals(20, calc.multiply(4, 5));
}
@Test
void testApplyDiscount() {
Calculator calc = new Calculator();
// คาดหวัง: 100 - 10 = 90.0 แต่โค้ดส่งคืน 80.0 -> เกิด AssertionFailedError!
assertEquals(90.0, calc.applyDiscount(100.0, 10.0), 0.001);
}
}
from core.client import OllamaLLMClient
from core.parser import StructuredOutputParser
from core.dispatcher import ToolDispatcher
from core.tools.test_runner import run_maven_test, BuildExecutionError
def main():
print("🚀 เริ่มทดสอบ Day 5: Maven Build & Test Runner Tool บน Mac M3...")
# 1. ติดตั้ง Dispatcher และจดทะเบียน Tool ทั้งสองตัว (File Reader + Maven Runner)
dispatcher = ToolDispatcher()
dispatcher.register("run_maven_test", run_maven_test)
client = OllamaLLMClient(model_name="qwen2.5-coder:7b")
if not client.check_health():
print("❌ ข้อผิดพลาด: ไม่พบ Ollama Daemon หรือโมเดล qwen2.5-coder:7b")
return
# 2. System Prompt ประกาศเครื่องมือรันเทส
system_prompt = (
"You are an Autonomous Software Engineering Agent.\n"
"You have tools to build and run automated tests for Java Maven projects.\n"
"When you need to test code, respond ONLY with a JSON action object.\n\n"
"Available Tools:\n"
"1. run_maven_test(project_dir: string, test_name: string|null): Runs Maven unit tests.\n\n"
"JSON Response Schema:\n"
"{\n"
' "tool": "run_maven_test",\n'
' "arguments": {\n'
' "project_dir": "path/to/project"\n'
' }\n'
"}"
)
user_query = "ช่วยรันเทสในโปรเจกต์ sample_maven_project ให้หน่อยว่าผ่านไหม และเกิดปัญหาอะไรขึ้น"
print(f"💬 User Prompt: {user_query}")
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_query}
]
# 3. Turn 1: ให้โมเดลขอเรียก Tool
print("\n📡 กำลังส่งโจทย์ให้โมเดลประมวลผล...")
raw_response = client.chat(messages, temperature=0.1, json_mode=True)
print(f"📥 โมเดลตัดสินใจส่ง JSON Action:\n{raw_response}")
try:
action = StructuredOutputParser.parse(raw_response)
tool_name = action.get("tool")
arguments = action.get("arguments", {})
print(f"\n⚙️ Dispatcher กำลังสั่งรัน Process '{tool_name}'...")
# 4. Dispatcher รันคำสั่ง mvn test จริงผ่าน Terminal
test_output = dispatcher.dispatch(tool_name, arguments)
print(f"✅ บันทึกผลลัพธ์จาก Process:\n{test_output}")
# 5. Turn 2: ส่งผลลัพธ์ Observation กลับไปให้โมเดลวิเคราะห์สาเหตุและแนวทางแก้ไข
messages.append({"role": "assistant", "content": raw_response})
messages.append({
"role": "user",
"content": f"Observation from run_maven_test:\n```text\n{test_output}\n```\nโปรดวิเคราะห์ว่าทำไมเทสถึงล้มเหลว คาดหวังค่าอะไรและได้ค่าอะไรกลับมา"
})
print("\n🧠 โมเดลกำลังอ่าน Stack Trace และสังเคราะห์คำตอบ...")
final_answer = client.chat(messages, temperature=0.2, json_mode=False)
print("\n🎯 สรุปผลการวิเคราะห์จาก AI Agent:")
print("--------------------------------------------------")
print(final_answer)
print("--------------------------------------------------")
print("🎉 สำเร็จ 100%! Agent ของคุณสามารถสั่งรันเทสจริงและจับบั๊กในระดับ OS Process ได้แล้ว!")
except BuildExecutionError as err:
print(f"❌ Build Execution Error: {err}")
except Exception as err:
print(f"❌ Error: {err}")
if __name__ == "__main__":
main()
เปิด Terminal บน MacBook M3 แล้วทำตาม 5 ขั้นตอนนี้เพื่อติดตั้งและทดสอบ Maven Test Runner ทันที:
mvn -version
หากยังไม่มี Maven ในเครื่อง สามารถติดตั้งผ่าน Homebrew ได้ทันที: brew install maven
cd ~/se_agent
source .venv/bin/activate
mkdir -p core/tools sample_maven_project/src/main/java sample_maven_project/src/test/java
touch core/tools/test_runner.py test_maven_runner.py sample_maven_project/pom.xml
touch sample_maven_project/src/main/java/Calculator.java sample_maven_project/src/test/java/CalculatorTest.java
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 1) ไปวางใน core/tools/test_runner.py
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 2) ไปวางใน sample_maven_project/pom.xml
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 3) ไปวางใน Calculator.java และ CalculatorTest.java
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 4) ไปวางใน test_maven_runner.py
python test_maven_runner.py
คุณจะเห็นโมเดลส่ง JSON ขอรันเทส ➔ Python ยิงคำสั่ง mvn test จริงใน Terminal ➔ พบว่า testApplyDiscount ล้มเหลว (Expected: 90.0, Actual: 80.0) ➔ AI สรุปสาเหตุของบั๊กใน Calculator.java ได้อย่างแม่นยำ 100%!
ลองเปิดไฟล์ Calculator.java แล้วแก้ไขสูตรเป็น return price - discount; จากนั้นรัน python test_maven_runner.py ซ้ำอีกครั้ง คุณจะเห็นโมเดลรายงานว่า BUILD SUCCESS การรันผ่านเรียบร้อย 100%!
ข้อที่ 1: บทบาทของ "Maven Build & Test Runner Tool" ต่อสถาปัตยกรรมของ SE Agent คืออะไร?
ข้อที่ 2: เหตุใดการสั่งรันคำสั่งภายนอกใน Python จึงควรใช้ `subprocess.run(["mvn", "test"])` แทน `os.system("mvn test")`?
ข้อที่ 3: ในการสั่งรันคำสั่งระบบ ค่า `returncode == 0` บ่งบอกถึงสิ่งใด?
ข้อที่ 4: การกำหนดพารามิเตอร์ `timeout=120` ใน `subprocess.run()` มีจุดประสงค์เพื่อป้องกันปัญหาใด?
ข้อที่ 5: เหตุใดในฟังก์ชัน `run_maven_test` จึงต้องมีการตัดทอน Log (Truncation) ก่อนส่งกลับให้โมเดล?
ข้อที่ 6: คำสั่ง `shutil.which("mvn")` ทำหน้าที่อะไรในกระบวนการตรวจสอบระบบ?
ข้อที่ 7: อาร์กิวเมนต์ `-B` ในคำสั่ง `mvn test -B` มีประโยชน์อย่างไรต่องานของ AI Agent?
ข้อที่ 8: เมื่อได้ผลลัพธ์ `BUILD FAILURE` พร้อม Stack Trace มาแล้ว ในลูป ReAct ขั้นตอนต่อไปคืออะไร?
ข้อที่ 9: พารามิเตอร์ `cwd=str(target_path)` ใน `subprocess.run()` มีหน้าที่สำคัญอย่างไร?
ข้อที่ 10: หากโฟลเดอร์เป้าหมายไม่มีไฟล์ `pom.xml` ฟังก์ชัน `run_maven_test` ควรตอบสนองอย่างไร?
ข้อที่ 11: การที่โมเดล `qwen2.5-coder:7b` สามารถอ่าน Stack Trace ของ JUnit และชี้จุดผิดในโค้ดได้ เกิดจากอะไร?
ข้อที่ 12: การออกแบบแยกโมดูล `core/tools/test_runner.py` ออกจาก `dispatcher.py` สอดคล้องกับหลักการใด?
ข้อที่ 13: เมื่อเรามีทั้ง Tool 1 (File Reader) และ Tool 2 (Maven Runner) ครบแล้ว ใน **Day 6 (ReAct Loop Engine)** เราจะสร้างอะไรต่อ?
ข้อที่ 14: ทำไมการรัน Maven Test บน Mac M3 แบบ Local ถึงสำคัญต่อความปลอดภัยของข้อมูลในระดับ Enterprise?
ข้อที่ 15: ในฐานะวิศวกรซอฟต์แวร์ เมื่อเห็นว่า Agent สั่งคอมไพล์โค้ดจริงและจับบั๊กออกมาได้ด้วยตัวเอง รู้สึกอย่างไร?
🏁 สรุปหมุดเชื่อมโยง Day 5 (Chapter Wrap-up)
ใน Day 5 — Building Tool 2: Maven Build & Test Runner คุณได้สร้างโมดูล core/tools/test_runner.py จัดการรัน OS Process ด้วย subprocess.run() ควบคุม Timeout สกัด Error Log และทดสอบรัน test_maven_runner.py เชื่อมโยงการตรวจจับบั๊กในโค้ด Java ได้จริง ตอนนี้ Agent ของคุณมีทั้ง ดวงตามองเห็นไฟล์ (File Reader) และ มือเท้าลงมือทดสอบโค้ด (Test Runner) ครบถ้วนแล้ว พร้อมก้าวเข้าสู่หัวใจสำคัญ: Day 6 — Building the Autonomous ReAct Loop Engine เพื่อให้สมองกลคิดและลงมือทำวนลูปอัตโนมัติจนงานเสร็จได้เองแล้วครับ!