add memory limit
This commit is contained in:
23
dsl/ast.go
23
dsl/ast.go
@@ -8,6 +8,7 @@ type File struct {
|
||||
BuildWindows string
|
||||
BuildDarwin string
|
||||
Timeout time.Duration
|
||||
MemoryLimit int64 // bytes; 0 means no limit
|
||||
Binary string // executable name produced by build (default: solution)
|
||||
Sources string // glob pattern for source files, expanded as $SOURCES in build
|
||||
|
||||
@@ -18,12 +19,13 @@ type File struct {
|
||||
}
|
||||
|
||||
type Group struct {
|
||||
Name string
|
||||
Weight float64
|
||||
Timeout time.Duration
|
||||
Env map[string]string
|
||||
Scoring ScoringMode
|
||||
Wrapper string // exec wrapper command (e.g., "valgrind --error-exitcode=1")
|
||||
Name string
|
||||
Weight float64
|
||||
Timeout time.Duration
|
||||
MemoryLimit int64
|
||||
Env map[string]string
|
||||
Scoring ScoringMode
|
||||
Wrapper string // exec wrapper command (e.g., "valgrind --error-exitcode=1")
|
||||
|
||||
Tests []*Test
|
||||
Pattern *Pattern
|
||||
@@ -50,10 +52,11 @@ func (p *Pattern) IsDirMode() bool {
|
||||
}
|
||||
|
||||
type Test struct {
|
||||
Name string
|
||||
Timeout time.Duration
|
||||
Env map[string]string
|
||||
Wrapper string
|
||||
Name string
|
||||
Timeout time.Duration
|
||||
MemoryLimit int64
|
||||
Env map[string]string
|
||||
Wrapper string
|
||||
|
||||
Stdin *string
|
||||
Args []string
|
||||
|
||||
38
dsl/lexer.go
38
dsl/lexer.go
@@ -14,6 +14,7 @@ const (
|
||||
TOKEN_FLOAT
|
||||
TOKEN_INT
|
||||
TOKEN_DURATION
|
||||
TOKEN_SIZE
|
||||
|
||||
TOKEN_LBRACE
|
||||
TOKEN_RBRACE
|
||||
@@ -37,6 +38,8 @@ func (t TokenType) String() string {
|
||||
return "INT"
|
||||
case TOKEN_DURATION:
|
||||
return "DURATION"
|
||||
case TOKEN_SIZE:
|
||||
return "SIZE"
|
||||
case TOKEN_LBRACE:
|
||||
return "{"
|
||||
case TOKEN_RBRACE:
|
||||
@@ -353,6 +356,10 @@ func (l *Lexer) readNumberOrDuration(line, col int) (Token, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if sizeSuffix := l.tryReadSizeSuffix(); sizeSuffix != "" {
|
||||
return Token{TOKEN_SIZE, buf.String() + sizeSuffix, line, col}, nil
|
||||
}
|
||||
|
||||
suffix := l.tryReadDurationSuffix()
|
||||
if suffix != "" {
|
||||
return Token{TOKEN_DURATION, buf.String() + suffix, line, col}, nil
|
||||
@@ -364,6 +371,37 @@ func (l *Lexer) readNumberOrDuration(line, col int) (Token, error) {
|
||||
return Token{TOKEN_INT, buf.String(), line, col}, nil
|
||||
}
|
||||
|
||||
// tryReadSizeSuffix reads memory size suffixes: B, K, KB, KiB, M, MB, MiB, G, GB, GiB.
|
||||
// Units are case-sensitive uppercase to avoid collision with duration "m" (minutes).
|
||||
func (l *Lexer) tryReadSizeSuffix() string {
|
||||
ch, ok := l.peek()
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
var unit rune
|
||||
switch ch {
|
||||
case 'B':
|
||||
l.advance()
|
||||
return "B"
|
||||
case 'K', 'M', 'G':
|
||||
unit = ch
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
l.advance()
|
||||
var buf strings.Builder
|
||||
buf.WriteRune(unit)
|
||||
if next, ok := l.peek(); ok && next == 'i' {
|
||||
l.advance()
|
||||
buf.WriteRune('i')
|
||||
}
|
||||
if next, ok := l.peek(); ok && next == 'B' {
|
||||
l.advance()
|
||||
buf.WriteRune('B')
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (l *Lexer) tryReadDurationSuffix() string {
|
||||
ch, ok := l.peek()
|
||||
if !ok {
|
||||
|
||||
95
dsl/memory_test.go
Normal file
95
dsl/memory_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package dsl
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseSizeLiteral(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
want int64
|
||||
}{
|
||||
{"256", 256},
|
||||
{"256B", 256},
|
||||
{"1K", 1024},
|
||||
{"2KB", 2 * 1024},
|
||||
{"4KiB", 4 * 1024},
|
||||
{"256M", 256 * 1024 * 1024},
|
||||
{"256MB", 256 * 1024 * 1024},
|
||||
{"512MiB", 512 * 1024 * 1024},
|
||||
{"1G", 1024 * 1024 * 1024},
|
||||
{"2GB", 2 * 1024 * 1024 * 1024},
|
||||
{"3GiB", 3 * 1024 * 1024 * 1024},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got, err := parseSizeLiteral(c.in, 0, 0)
|
||||
if err != nil {
|
||||
t.Errorf("parseSizeLiteral(%q) error: %v", c.in, err)
|
||||
continue
|
||||
}
|
||||
if got != c.want {
|
||||
t.Errorf("parseSizeLiteral(%q) = %d, want %d", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSizeLiteralInvalid(t *testing.T) {
|
||||
bad := []string{"abc", "100TB", "10XB", ""}
|
||||
for _, s := range bad {
|
||||
if _, err := parseSizeLiteral(s, 0, 0); err == nil {
|
||||
t.Errorf("parseSizeLiteral(%q) expected error", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMemoryLimit(t *testing.T) {
|
||||
src := `
|
||||
build "go build -o solution ."
|
||||
timeout 10s
|
||||
memory_limit = 256MB
|
||||
|
||||
group("g1") {
|
||||
weight = 0.5
|
||||
memory_limit = 128MiB
|
||||
|
||||
test("t1") {
|
||||
stdout = "ok\n"
|
||||
}
|
||||
|
||||
test("t2") {
|
||||
memory_limit = 64M
|
||||
stdout = "ok\n"
|
||||
}
|
||||
}
|
||||
|
||||
group("g2") {
|
||||
weight = 0.5
|
||||
|
||||
test("inherits") {
|
||||
stdout = "ok\n"
|
||||
}
|
||||
}
|
||||
`
|
||||
f, _, err := Parse(src)
|
||||
if err != nil {
|
||||
t.Fatalf("parse error: %v", err)
|
||||
}
|
||||
if f.MemoryLimit != 256*1024*1024 {
|
||||
t.Errorf("file memory: got %d", f.MemoryLimit)
|
||||
}
|
||||
g1 := f.Groups[0]
|
||||
if g1.MemoryLimit != 128*1024*1024 {
|
||||
t.Errorf("g1 memory: got %d", g1.MemoryLimit)
|
||||
}
|
||||
if g1.Tests[0].MemoryLimit != 128*1024*1024 {
|
||||
t.Errorf("g1.t1 memory (inherited from group): got %d", g1.Tests[0].MemoryLimit)
|
||||
}
|
||||
if g1.Tests[1].MemoryLimit != 64*1024*1024 {
|
||||
t.Errorf("g1.t2 memory (override): got %d", g1.Tests[1].MemoryLimit)
|
||||
}
|
||||
g2 := f.Groups[1]
|
||||
if g2.MemoryLimit != 256*1024*1024 {
|
||||
t.Errorf("g2 memory (inherited from file): got %d", g2.MemoryLimit)
|
||||
}
|
||||
if g2.Tests[0].MemoryLimit != 256*1024*1024 {
|
||||
t.Errorf("g2.inherits memory: got %d", g2.Tests[0].MemoryLimit)
|
||||
}
|
||||
}
|
||||
120
dsl/parser.go
120
dsl/parser.go
@@ -169,8 +169,19 @@ func (p *Parser) parseFile() (*File, error) {
|
||||
}
|
||||
f.Timeout = d
|
||||
|
||||
case "memory_limit":
|
||||
p.advance()
|
||||
if _, err := p.expect(TOKEN_ASSIGN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n, err := p.parseSize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.MemoryLimit = n
|
||||
|
||||
case "group":
|
||||
g, err := p.parseGroup(f.Timeout)
|
||||
g, err := p.parseGroup(f.Timeout, f.MemoryLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -202,7 +213,7 @@ func (p *Parser) validateWeights(f *File) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseGroup(defaultTimeout time.Duration) (*Group, error) {
|
||||
func (p *Parser) parseGroup(defaultTimeout time.Duration, defaultMemory int64) (*Group, error) {
|
||||
if err := p.expectIdent("group"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -221,10 +232,11 @@ func (p *Parser) parseGroup(defaultTimeout time.Duration) (*Group, error) {
|
||||
}
|
||||
|
||||
g := &Group{
|
||||
Name: name.Value,
|
||||
Timeout: defaultTimeout,
|
||||
Env: map[string]string{},
|
||||
Scoring: ScoringPartial,
|
||||
Name: name.Value,
|
||||
Timeout: defaultTimeout,
|
||||
MemoryLimit: defaultMemory,
|
||||
Env: map[string]string{},
|
||||
Scoring: ScoringPartial,
|
||||
}
|
||||
|
||||
for !p.isRBrace() {
|
||||
@@ -256,6 +268,17 @@ func (p *Parser) parseGroup(defaultTimeout time.Duration) (*Group, error) {
|
||||
}
|
||||
g.Timeout = d
|
||||
|
||||
case "memory_limit":
|
||||
p.advance()
|
||||
if _, err := p.expect(TOKEN_ASSIGN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n, err := p.parseSize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g.MemoryLimit = n
|
||||
|
||||
case "scoring":
|
||||
p.advance()
|
||||
if _, err := p.expect(TOKEN_ASSIGN); err != nil {
|
||||
@@ -307,7 +330,7 @@ func (p *Parser) parseGroup(defaultTimeout time.Duration) (*Group, error) {
|
||||
g.Wrapper = s.Value
|
||||
|
||||
case "test":
|
||||
test, err := p.parseTest(g.Timeout)
|
||||
test, err := p.parseTest(g.Timeout, g.MemoryLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -331,7 +354,7 @@ func (p *Parser) parseGroup(defaultTimeout time.Duration) (*Group, error) {
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseTest(defaultTimeout time.Duration) (*Test, error) {
|
||||
func (p *Parser) parseTest(defaultTimeout time.Duration, defaultMemory int64) (*Test, error) {
|
||||
if err := p.expectIdent("test"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -351,14 +374,15 @@ func (p *Parser) parseTest(defaultTimeout time.Duration) (*Test, error) {
|
||||
|
||||
zero := 0
|
||||
test := &Test{
|
||||
Name: name.Value,
|
||||
Timeout: defaultTimeout,
|
||||
Env: map[string]string{},
|
||||
InFiles: map[string]string{},
|
||||
OutFiles: map[string]string{},
|
||||
ExitCode: &zero,
|
||||
Stdout: NoMatcher{},
|
||||
Stderr: NoMatcher{},
|
||||
Name: name.Value,
|
||||
Timeout: defaultTimeout,
|
||||
MemoryLimit: defaultMemory,
|
||||
Env: map[string]string{},
|
||||
InFiles: map[string]string{},
|
||||
OutFiles: map[string]string{},
|
||||
ExitCode: &zero,
|
||||
Stdout: NoMatcher{},
|
||||
Stderr: NoMatcher{},
|
||||
}
|
||||
|
||||
for !p.isRBrace() {
|
||||
@@ -428,6 +452,17 @@ func (p *Parser) parseTest(defaultTimeout time.Duration) (*Test, error) {
|
||||
}
|
||||
test.Timeout = d
|
||||
|
||||
case "memory_limit":
|
||||
p.advance()
|
||||
if _, err := p.expect(TOKEN_ASSIGN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n, err := p.parseSize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
test.MemoryLimit = n
|
||||
|
||||
case "wrapper":
|
||||
p.advance()
|
||||
if _, err := p.expect(TOKEN_ASSIGN); err != nil {
|
||||
@@ -680,6 +715,59 @@ func (p *Parser) parseInt() (int, error) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// parseSize accepts either a TOKEN_SIZE (e.g. "256MB", "1GiB", "512K") or a bare
|
||||
// TOKEN_INT interpreted as bytes. MiB/MB are both 1024² — we use IEC semantics.
|
||||
func (p *Parser) parseSize() (int64, error) {
|
||||
t := p.peek()
|
||||
switch t.Type {
|
||||
case TOKEN_SIZE:
|
||||
p.advance()
|
||||
return parseSizeLiteral(t.Value, t.Line, t.Col)
|
||||
case TOKEN_INT:
|
||||
p.advance()
|
||||
n, err := strconv.ParseInt(t.Value, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%d:%d: invalid size %q", t.Line, t.Col, t.Value)
|
||||
}
|
||||
return n, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("%d:%d: expected size (e.g. 256MB, 1GiB), got %s %q", t.Line, t.Col, t.Type, t.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func parseSizeLiteral(s string, line, col int) (int64, error) {
|
||||
i := 0
|
||||
for i < len(s) && (s[i] >= '0' && s[i] <= '9') {
|
||||
i++
|
||||
}
|
||||
if i == 0 {
|
||||
return 0, fmt.Errorf("%d:%d: invalid size %q", line, col, s)
|
||||
}
|
||||
numPart := s[:i]
|
||||
unit := s[i:]
|
||||
n, err := strconv.ParseInt(numPart, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%d:%d: invalid size %q", line, col, s)
|
||||
}
|
||||
var mult int64
|
||||
switch unit {
|
||||
case "", "B":
|
||||
mult = 1
|
||||
case "K", "KB", "KiB":
|
||||
mult = 1024
|
||||
case "M", "MB", "MiB":
|
||||
mult = 1024 * 1024
|
||||
case "G", "GB", "GiB":
|
||||
mult = 1024 * 1024 * 1024
|
||||
default:
|
||||
return 0, fmt.Errorf("%d:%d: unknown size unit %q (use B/K/M/G or KiB/MiB/GiB)", line, col, unit)
|
||||
}
|
||||
if n < 0 {
|
||||
return 0, fmt.Errorf("%d:%d: size must be non-negative", line, col)
|
||||
}
|
||||
return n * mult, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseDuration() (time.Duration, error) {
|
||||
t := p.peek()
|
||||
if t.Type != TOKEN_DURATION {
|
||||
|
||||
Reference in New Issue
Block a user