d493be34b2
PythonDateParser shells out to python3 with the dateparser library for full natural-language date/time extraction. Two-step approach: 1. search_dates() finds the date substring in surrounding text 2. parse() re-parses the substring for correct time resolution Russian time qualifiers (утра/вечера/дня/ночи) are pre-processed to AM/PM before parsing — dateparser drops them during substring extraction. Falls back to StubDateTimeParser when python3 or dateparser isn't available (graceful degradation, no hard runtime dependency). Dockerfile updated: python3 + dateparser==1.4.1 in runtime stage.
158 lines
4.7 KiB
Go
158 lines
4.7 KiB
Go
package router
|
|
|
|
import (
|
|
"context"
|
|
"os/exec"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestPythonDateParser requires python3 + dateparser installed. Skips
|
|
// gracefully in environments without them (e.g. bare go test, dev machines
|
|
// without the Docker runtime image).
|
|
func TestPythonDateParser(t *testing.T) {
|
|
// Skip if python3 isn't on PATH.
|
|
if _, err := exec.LookPath("python3"); err != nil {
|
|
t.Skip("python3 not on PATH — skipping dateparser tests")
|
|
}
|
|
// Skip if dateparser isn't installed.
|
|
cmd := exec.Command("python3", "-c", "import dateparser")
|
|
if err := cmd.Run(); err != nil {
|
|
t.Skip("python dateparser not installed — skipping dateparser tests")
|
|
}
|
|
|
|
p := NewPythonDateParser()
|
|
now := time.Date(2026, 7, 6, 18, 0, 0, 0, time.Local)
|
|
ctx := context.Background()
|
|
|
|
tests := []struct {
|
|
name string
|
|
text string
|
|
wantOK bool
|
|
checkT func(t *testing.T, got, now time.Time)
|
|
}{
|
|
{
|
|
name: "ru relative — через час",
|
|
text: "напомни через час выпить воды",
|
|
wantOK: true,
|
|
checkT: func(t *testing.T, got, now time.Time) {
|
|
d := got.Sub(now)
|
|
if d < 50*time.Minute || d > 70*time.Minute {
|
|
t.Errorf("через час: got %v from now, want ~1h", d)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "ru relative — через 30 минут",
|
|
text: "напомни через 30 минут снять бельё",
|
|
wantOK: true,
|
|
checkT: func(t *testing.T, got, now time.Time) {
|
|
d := got.Sub(now)
|
|
if d < 25*time.Minute || d > 35*time.Minute {
|
|
t.Errorf("через 30 минут: got %v from now, want ~30m", d)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "ru absolute — завтра в 9 утра",
|
|
text: "напомни завтра в 9 утра позвонить",
|
|
wantOK: true,
|
|
checkT: func(t *testing.T, got, now time.Time) {
|
|
if got.Hour() != 9 {
|
|
t.Errorf("завтра в 9 утра: hour=%d, want 9", got.Hour())
|
|
}
|
|
if got.Sub(now) < 12*time.Hour {
|
|
t.Errorf("завтра в 9 утра: only %v from now, want >12h (tomorrow)", got.Sub(now))
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "ru complex — в следующую пятницу",
|
|
text: "напомни в следующую пятницу оплатить счёт",
|
|
wantOK: true,
|
|
checkT: func(t *testing.T, got, now time.Time) {
|
|
if got.Weekday() != time.Friday {
|
|
t.Errorf("в следующую пятницу: weekday=%v, want Friday", got.Weekday())
|
|
}
|
|
if got.Sub(now) < 24*time.Hour {
|
|
t.Errorf("в следующую пятницу: only %v from now, want >24h (future)", got.Sub(now))
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "en relative — in 30 minutes",
|
|
text: "remind me in 30 minutes to drink water",
|
|
wantOK: true,
|
|
checkT: func(t *testing.T, got, now time.Time) {
|
|
d := got.Sub(now)
|
|
if d < 25*time.Minute || d > 35*time.Minute {
|
|
t.Errorf("in 30 minutes: got %v from now, want ~30m", d)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "en absolute — tomorrow at 8am",
|
|
text: "remind me tomorrow at 8am to call the doctor",
|
|
wantOK: true,
|
|
checkT: func(t *testing.T, got, now time.Time) {
|
|
if got.Hour() != 8 {
|
|
t.Errorf("tomorrow at 8am: hour=%d, want 8", got.Hour())
|
|
}
|
|
if got.Sub(now) < 12*time.Hour {
|
|
t.Errorf("tomorrow at 8am: only %v from now, want >12h (tomorrow)", got.Sub(now))
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "no date — напомни мне",
|
|
text: "напомни мне",
|
|
wantOK: false,
|
|
},
|
|
{
|
|
name: "no date — remind me",
|
|
text: "remind me to do something",
|
|
wantOK: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, ok, err := p.Parse(ctx, tt.text, now)
|
|
if err != nil {
|
|
t.Fatalf("Parse(%q) error: %v", tt.text, err)
|
|
}
|
|
if ok != tt.wantOK {
|
|
t.Fatalf("Parse(%q) ok=%v, want %v (got time=%v)", tt.text, ok, tt.wantOK, got)
|
|
}
|
|
if ok && tt.checkT != nil {
|
|
tt.checkT(t, got, now)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPythonDateParser_FallbackToStub verifies that when python3 is not
|
|
// available, the parser falls back to StubDateTimeParser instead of
|
|
// returning an error.
|
|
func TestPythonDateParser_FallbackToStub(t *testing.T) {
|
|
if _, err := exec.LookPath("python3"); err == nil {
|
|
t.Skip("python3 is available — fallback path not exercised")
|
|
}
|
|
p := NewPythonDateParser()
|
|
ctx := context.Background()
|
|
now := time.Date(2026, 7, 6, 18, 0, 0, 0, time.Local)
|
|
|
|
// The stub handles "in 1 hour" — should still work via fallback.
|
|
got, ok, err := p.Parse(ctx, "in 1 hour", now)
|
|
if err != nil {
|
|
t.Fatalf("fallback Parse error: %v", err)
|
|
}
|
|
if !ok {
|
|
t.Fatal("fallback Parse: ok=false, want true (stub should handle 'in 1 hour')")
|
|
}
|
|
d := got.Sub(now)
|
|
if d < 50*time.Minute || d > 70*time.Minute {
|
|
t.Errorf("fallback 'in 1 hour': got %v from now, want ~1h", d)
|
|
}
|
|
}
|