From 330a94bd02c65da5ff1dfb9bf7545f87807d9f3d Mon Sep 17 00:00:00 2001 From: sudonter Date: Thu, 22 Jan 2026 09:46:32 -0500 Subject: [PATCH 1/3] Add buffers, wfs Fixes bitset32 tests Adds per PR testing --- .github/workflows/test-branch.yaml | 28 ++++++ buffers/rotate.go | 136 +++++++++++++++++++++++++++++ buffers/rotate_test.go | 34 ++++++++ dontio/filestd.go | 3 - files/wfs.go | 29 ++++++ go.mod | 4 +- mirrors/types.go | 5 +- skelly/bitset32/bits_test.go | 32 +++---- 8 files changed, 246 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/test-branch.yaml create mode 100644 buffers/rotate.go create mode 100644 buffers/rotate_test.go create mode 100644 files/wfs.go diff --git a/.github/workflows/test-branch.yaml b/.github/workflows/test-branch.yaml new file mode 100644 index 0000000..f94adf7 --- /dev/null +++ b/.github/workflows/test-branch.yaml @@ -0,0 +1,28 @@ +name: Test Branch +on: + pull_request: + types: + - opened + - reopened + - synchronize +concurrency: + group: branch-test-${{ github.head_ref }} + cancel-in-progress: true +jobs: + build-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version-file: "go.mod" + - shell: bash + run: | + go install honnef.co/go/tools/cmd/staticcheck@latest || exit 2 + exec go vet ./... + - shell: bash + if: ${{ !cancelled() }} + run: exec staticcheck ./... + - shell: bash + if: ${{ !cancelled() }} + run: exec go test -race -vet=off ./... diff --git a/buffers/rotate.go b/buffers/rotate.go new file mode 100644 index 0000000..89eb973 --- /dev/null +++ b/buffers/rotate.go @@ -0,0 +1,136 @@ +package buffers + +import ( + "bytes" + "errors" + "io" + "os" + "sync" + + "github.com/etc-sudonters/substrate/files" + "github.com/etc-sudonters/substrate/slipup" +) + +type memorybuffer struct { + *bytes.Buffer +} + +func (_ memorybuffer) Close() error { + return nil +} + +type Memory struct{} + +func (_ *Memory) CreateBuffer(sizehint uint64) (io.WriteCloser, error) { + return memorybuffer{bytes.NewBuffer(make([]byte, 0, sizehint))}, nil +} + +type FileSystem struct { + naming func() string + fs files.OpenFS +} + +func (this *FileSystem) CreateBuffer(uint64) (io.WriteCloser, error) { + return this.fs.OpenFile(this.naming(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) +} + +func NewFileSystem(fs files.OpenFS, naming func() string) *FileSystem { + fsbufs := new(FileSystem) + fsbufs.naming = naming + fsbufs.fs = fs + return fsbufs +} + +type BufferCreator interface { + CreateBuffer(sizehint uint64) (io.WriteCloser, error) +} + +func NewRotater(creator BufferCreator, maxBytes uint64) (*Rotater, error) { + rotater := new(Rotater) + rotater.m = new(sync.Mutex) + rotater.maxbytes = maxBytes + rotater.newbuffer = creator + + if err := rotater.rotate(); err != nil { + return rotater, slipup.Describe(err, "failed to initialize buffer rotation") + } + + return rotater, nil +} + +type Rotater struct { + newbuffer BufferCreator + maxbytes uint64 + curr *rotatingbuffer + m *sync.Mutex +} + +func (this *Rotater) Rotate() error { + this.m.Lock() + defer this.m.Unlock() + return this.rotate() +} + +func (this *Rotater) Write(b []byte) (int, error) { + this.m.Lock() + defer this.m.Unlock() + return this.write(b) +} + +func (this *Rotater) write(b []byte) (int, error) { + if this.curr == nil || this.shouldRotate(b) { + if rotateErr := this.rotate(); rotateErr != nil { + return 0, slipup.Describe(rotateErr, "failed to rotate buffer") + } + } + + return this.curr.Write(b) +} + +func (this *Rotater) shouldRotate(b []byte) bool { + return this.curr.n+uint64(len(b)) >= this.maxbytes +} + +func (this *Rotater) rotate() error { + if this.curr != nil { + this.curr.Close() + this.curr = nil + } + + buffer, bufferErr := this.newbuffer.CreateBuffer(this.maxbytes) + if bufferErr != nil { + return slipup.Describe(bufferErr, "failed to initialize buffer") + } + + this.curr = &rotatingbuffer{buffer, 0, nil, false} + return nil +} + +type rotatingbuffer struct { + w io.WriteCloser + n uint64 + err error + closed bool +} + +var errBufferClosed = errors.New("buffer already closed") + +func (this *rotatingbuffer) Write(b []byte) (int, error) { + if this.closed { + return 0, errBufferClosed + } + if this.err != nil { + return 0, this.err + } + n, err := this.w.Write(b) + this.n += uint64(n) + this.err = err + return n, err +} + +func (this *rotatingbuffer) Close() { + if !this.closed { + this.closed = true + this.w.Close() + } +} diff --git a/buffers/rotate_test.go b/buffers/rotate_test.go new file mode 100644 index 0000000..7159061 --- /dev/null +++ b/buffers/rotate_test.go @@ -0,0 +1,34 @@ +package buffers + +import ( + "io" + "log/slog" + "strings" + "testing" +) + +type counter struct { + inner BufferCreator + n int +} + +func (this *counter) CreateBuffer(hint uint64) (io.WriteCloser, error) { + this.n += 1 + return this.inner.CreateBuffer(hint) +} + +func TestBufferRotation(t *testing.T) { + buffers := &counter{&Memory{}, 0} + rotation, err := NewRotater(buffers, 64) + if err != nil { + t.Fatal(err) + } + handler := slog.NewTextHandler(rotation, nil) + logger := slog.New(handler) + logger.Warn(strings.Repeat("a", 34)) + logger.Warn(strings.Repeat("a", 34)) + + if buffers.n <= 1 { + t.Fatalf("expected to create at least 2 buffers but only created %v", buffers.n) + } +} diff --git a/dontio/filestd.go b/dontio/filestd.go index 48477a8..33f1119 100644 --- a/dontio/filestd.go +++ b/dontio/filestd.go @@ -6,8 +6,6 @@ import ( "path/filepath" ) -type FileStdFlags uint64 - // populates the passed *Std with files for out, err the returned cleanup // function will never be nil and must always be called func FileStd(std *Std, dir string) (func(), error) { @@ -36,5 +34,4 @@ func FileStd(std *Std, dir string) (func(), error) { std.Out = opened[0] std.Err = opened[1] return cleanup, nil - } diff --git a/files/wfs.go b/files/wfs.go new file mode 100644 index 0000000..bdb5058 --- /dev/null +++ b/files/wfs.go @@ -0,0 +1,29 @@ +package files + +import ( + "io/fs" + "os" +) + +type WriteableFile interface { + fs.File + Write([]byte) (int, error) + Close() error +} + +type OpenFS interface { + fs.FS + OpenFile(name string, flag int, perm fs.FileMode) (WriteableFile, error) +} + +var OsFS OpenFS = &osfs{} + +type osfs struct{} + +func (this *osfs) Open(name string) (fs.File, error) { + return this.OpenFile(name, os.O_RDONLY, 0) +} + +func (this *osfs) OpenFile(name string, flag int, perm fs.FileMode) (WriteableFile, error) { + return os.OpenFile(name, flag, perm) +} diff --git a/go.mod b/go.mod index ea7fe4f..527b356 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,3 @@ module github.com/etc-sudonters/substrate -go 1.23.0 - -require golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect +go 1.25.0 diff --git a/mirrors/types.go b/mirrors/types.go index 2f56d5b..52e3da4 100644 --- a/mirrors/types.go +++ b/mirrors/types.go @@ -3,12 +3,11 @@ package mirrors import "reflect" func T[T any]() reflect.Type { - return TypeOf[T]() + return reflect.TypeFor[T]() } func TypeOf[T any]() reflect.Type { - var t T - return reflect.TypeOf(t) + return reflect.TypeFor[T]() } func Empty[T any]() T { diff --git a/skelly/bitset32/bits_test.go b/skelly/bitset32/bits_test.go index 10f7784..970fb94 100644 --- a/skelly/bitset32/bits_test.go +++ b/skelly/bitset32/bits_test.go @@ -14,8 +14,8 @@ func TestSetsBits(t *testing.T) { numbers := []uint32{ 1, + 33, 65, - 129, } b := Bitset{} @@ -34,11 +34,11 @@ func TestClearsBits(t *testing.T) { expected := []uint32{2, 0, 2} b := Bitset{} - numbers := []uint32{1, 65, 129} + numbers := []uint32{1, 33, 65} for i := range numbers { b.Set(numbers[i]) } - b.Unset(65) + b.Unset(33) for i := range expected { if expected[i] != b.buckets[i] { @@ -56,13 +56,13 @@ func TestTestBits(t *testing.T) { t.Fail() } - if !b.IsSet(65) { - t.Log("expected 65 to be set") + if !b.IsSet(33) { + t.Log("expected 33 to be set") t.Fail() } - if !b.IsSet(129) { - t.Log("expected 129 to be set") + if !b.IsSet(65) { + t.Log("expected 65 to be set") t.Fail() } } @@ -70,8 +70,8 @@ func TestTestBits(t *testing.T) { func TestComplement(t *testing.T) { b := Bitset{} b.Set(1) + b.Set(33) b.Set(65) - b.Set(129) comp := b.Complement().buckets expected := maxU ^ 2 @@ -85,7 +85,7 @@ func TestIntersect(t *testing.T) { b1 := Bitset{} b2 := Bitset{} - shared := []uint32{1, 65, 129} + shared := []uint32{1, 33, 65} b1.Set(144) b2.Set(13) @@ -107,8 +107,8 @@ func TestUnion(t *testing.T) { b3 := Bitset{} b1.Set(1) - b2.Set(65) - b3.Set(129) + b2.Set(33) + b3.Set(65) b := b1.Union(b2).Union(b3) @@ -122,9 +122,9 @@ func TestDifference(t *testing.T) { b2 := Bitset{} b1.Set(1) - b1.Set(65) + b1.Set(33) + b2.Set(33) b2.Set(65) - b2.Set(129) b1DiffB2 := b1.Difference(b2) expected := FromRaw([]uint32{2, 0, 0}) @@ -138,7 +138,7 @@ func TestDifference(t *testing.T) { expected = FromRaw([]uint32{0, 0, 2}) if !b2DiffB1.Eq(expected) { - t.Log("expected only 129 to be set") + t.Log("expected only 65 to be set") t.Fail() } } @@ -146,10 +146,10 @@ func TestDifference(t *testing.T) { func TestElems(t *testing.T) { b := Bitset{} b.Set(1) + b.Set(33) b.Set(65) - b.Set(129) - expected := []uint32{1, 65, 129} + expected := []uint32{1, 33, 65} elems := b.Elems() if len(expected) != len(elems) { From fba4407c1b03553c38a439e9df700fa312d28763 Mon Sep 17 00:00:00 2001 From: sudonter Date: Thu, 22 Jan 2026 09:51:30 -0500 Subject: [PATCH 2/3] Fix staticcheck issues --- peruse/parser.go | 1 - skelly/stack/sized.go | 12 ++++++------ stageleft/codes.go | 5 ----- staticcheck.conf | 13 +++++++++++++ 4 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 staticcheck.conf diff --git a/peruse/parser.go b/peruse/parser.go index 036a0a5..8469a94 100644 --- a/peruse/parser.go +++ b/peruse/parser.go @@ -77,7 +77,6 @@ type Parser[T any] struct { g Grammar[T] l *StringLexer Cur, Next Token - empty T } func (p *Parser[T]) HasMore() bool { diff --git a/skelly/stack/sized.go b/skelly/stack/sized.go index 46b2dbb..97d072c 100644 --- a/skelly/stack/sized.go +++ b/skelly/stack/sized.go @@ -20,19 +20,19 @@ type Sized[T any] struct { ptr int } -func (this *Sized[T]) reset() { +func (this *Sized[T]) Reset() { this.ptr = 0 } -func (this *Sized[T]) slice(start, count int) []T { +func (this *Sized[T]) Slice(start, count int) []T { return this.items[start : start+count] } -func (this *Sized[T]) popN(n int) { +func (this *Sized[T]) PopN(n int) { this.ptr -= n } -func (this *Sized[T]) top() T { +func (this *Sized[T]) Top() T { if this.ptr < 1 { panic(ErrStackEmpty) } @@ -40,7 +40,7 @@ func (this *Sized[T]) top() T { return this.items[this.ptr-1] } -func (this *Sized[T]) push(item T) { +func (this *Sized[T]) Push(item T) { if this.ptr == len(this.items) { panic(ErrStackFull) } @@ -49,7 +49,7 @@ func (this *Sized[T]) push(item T) { this.ptr++ } -func (this *Sized[T]) pop() T { +func (this *Sized[T]) Pop() T { if this.ptr == 0 { panic(ErrStackEmpty) } diff --git a/stageleft/codes.go b/stageleft/codes.go index 2fd03e8..889f31f 100644 --- a/stageleft/codes.go +++ b/stageleft/codes.go @@ -31,19 +31,14 @@ func AsExitCode(r interface{}, exit ExitCode) ExitCode { switch r := r.(type) { case ExitCode: exit = r - break case *ExitCode: exit = *r - break case ExitCodeError: exit = r.Code - break case error: exit = ExitCodeFromErr(r, exit) - break case uint8: exit = ExitCode(r) - break } return exit diff --git a/staticcheck.conf b/staticcheck.conf new file mode 100644 index 0000000..0029625 --- /dev/null +++ b/staticcheck.conf @@ -0,0 +1,13 @@ +checks = [ + "all", "-SA9003", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", + "-ST1022", "-ST1023", "-ST1006" +] +initialisms = [ + "ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", + "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", + "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", + "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS", "SIP", "RTP", "AMQP", + "DB", "TS" +] +dot_import_whitelist = [] +http_status_code_whitelist = ["200", "400", "404", "500"] From fcd8715524a7b4730edb9787572a43b8e90579b6 Mon Sep 17 00:00:00 2001 From: sudonter Date: Thu, 22 Jan 2026 09:52:39 -0500 Subject: [PATCH 3/3] Additional workflows --- .github/workflows/actionlint.yaml | 22 ++++++++++++++++++++++ .github/workflows/test.yaml | 10 ++++++++++ 2 files changed, 32 insertions(+) create mode 100644 .github/workflows/actionlint.yaml create mode 100644 .github/workflows/test.yaml diff --git a/.github/workflows/actionlint.yaml b/.github/workflows/actionlint.yaml new file mode 100644 index 0000000..b2a68fd --- /dev/null +++ b/.github/workflows/actionlint.yaml @@ -0,0 +1,22 @@ +name: Lint GitHub Actions workflows +on: + pull_request: + types: + - opened + - reopened + - synchronize +concurrency: + group: branch-workflow-lint-${{ github.head_ref }} + cancel-in-progress: true +jobs: + actionlint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version-file: "go.mod" + - shell: bash + run: | + go install github.com/rhysd/actionlint/cmd/actionlint@latest + actionlint -format '{{range $err := .}}::error file={{$err.Filepath}},line={{$err.Line}},col={{$err.Column}}::{{$err.Message}}%0A```%0A{{replace $err.Snippet "\\n" "%0A"}}%0A```\n{{end}}' -ignore 'SC2016:' diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..3376cc3 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,10 @@ +name: Test +on: + workflow_dispatch: +jobs: + hello: + runs-on: ubuntu-latest + name: Test + steps: + - shell: bash + run: echo "Hello world"