From 80cdec3927ea866aea27ec356ae1d3f525ae94d7 Mon Sep 17 00:00:00 2001 From: EuAndreh Date: Wed, 14 Aug 2024 17:11:33 -0300 Subject: Use "go tool" to build project --- .gitignore | 3 + Makefile | 67 ++++-- src/cmd/main.go | 7 - src/lib.go | 582 -------------------------------------------------- src/main.go | 7 + src/papod.go | 583 +++++++++++++++++++++++++++++++++++++++++++++++++++ tests/integration.sh | 4 + tests/lib_test.go | 514 --------------------------------------------- tests/main.go | 7 + tests/papod.go | 540 +++++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 1194 insertions(+), 1120 deletions(-) delete mode 100644 src/cmd/main.go delete mode 100644 src/lib.go create mode 100644 src/main.go create mode 100644 src/papod.go create mode 100755 tests/integration.sh delete mode 100644 tests/lib_test.go create mode 100644 tests/main.go create mode 100644 tests/papod.go diff --git a/.gitignore b/.gitignore index 07707d5..fc8199b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ /*.bin +/src/*.a +/src/*.bin +/tests/*.a /tests/*.bin /papod.db diff --git a/Makefile b/Makefile index ba9e48f..73d81a2 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ LANGUAGES = en PREFIX = /usr BINDIR = $(PREFIX)/bin LIBDIR = $(PREFIX)/lib +GOLIBDIR = $(LIBDIR)/go INCLUDEDIR = $(PREFIX)/include SRCDIR = $(PREFIX)/src/$(NAME) SHAREDIR = $(PREFIX)/share @@ -17,14 +18,12 @@ DATADIR = $(SHAREDIR)/$(NAME) EXEC = ./ ## Where to store the installation. Empty by default. DESTDIR = -LDLIBS = -GOFLAGS = -ldflags '-extldflags "-static -lm"' -tags 'linux libsqlite3 \ - json1 fts5 sqlite_foreign_keys sqlite_omit_load_extension' +LDLIBS = -lsqlite3 .SUFFIXES: -.SUFFIXES: .go .bin +.SUFFIXES: .go .a .bin .bin-check @@ -32,16 +31,24 @@ all: include deps.mk +objects = \ + src/$(NAME).a \ + src/main.a \ + tests/$(NAME).a \ + tests/main.a \ + sources = \ - src/lib.go \ - src/cmd/main.go \ + src/$(NAME).go \ + src/main.go \ $(sources.static) \ $(sources.sql) \ derived-assets = \ + $(objects) \ + src/main.bin \ + tests/main.bin \ $(NAME).bin \ - tests/lib_test.bin \ side-assets = \ papod.public.socket \ @@ -57,22 +64,44 @@ side-assets = \ all: $(derived-assets) -$(NAME).bin: src/lib.go src/cmd/main.go Makefile - env CGO_ENABLED=1 go build $(GOFLAGS) -v -o $@ src/cmd/main.go +$(objects): Makefile + +src/$(NAME).a: src/$(NAME).go +src/main.a: src/main.go src/$(NAME).a +tests/main.a: tests/main.go tests/$(NAME).a +src/$(NAME).a src/main.a tests/main.a: + go tool compile $(GOCFLAGS) -o $@ -p $(*F) -I $(@D) $*.go + +tests/$(NAME).a: tests/$(NAME).go src/$(NAME).go + go tool compile $(GOCFLAGS) -o $@ -p $(*F) $*.go src/$(*F).go + +src/main.bin: src/main.a +tests/main.bin: tests/main.a +src/main.bin tests/main.bin: + go tool link $(GOLDFLAGS) -o $@ -L $(@D) --extldflags '$(LDLIBS)' $*.a + +$(NAME).bin: src/main.bin + ln -fs $? $@ + -tests/lib_test.bin: src/lib.go tests/lib_test.go Makefile - env CGO_ENABLED=1 go test $(GOFLAGS) -v -o $@ -c $*.go +tests.bin-check = \ + tests/main.bin-check \ +tests/main.bin-check: tests/main.bin +$(tests.bin-check): + $(EXEC)$*.bin -check-unit: tests/lib_test.bin - ./tests/lib_test.bin +check-unit: $(tests.bin-check) integration-tests = \ tests/cli-opts.sh \ + tests/integration.sh \ -$(integration-tests): $(NAME).bin ALWAYS +.PRECIOUS: $(integration-tests) +$(integration-tests): $(NAME).bin +$(integration-tests): ALWAYS sh $@ check-integration: $(integration-tests) @@ -96,8 +125,11 @@ clean: install: all mkdir -p \ '$(DESTDIR)$(BINDIR)' \ + '$(DESTDIR)$(GOLIBDIR)' \ + '$(DESTDIR)$(SRCDIR)' \ cp $(NAME).bin '$(DESTDIR)$(BINDIR)'/$(NAME) + cp src/$(NAME).a '$(DESTDIR)$(GOLIBDIR)' for f in $(sources.sql) $(sources.static); do \ dir='$(DESTDIR)$(DATADIR)'/"`dirname "$${f#src/}"`"; \ mkdir -p "$$dir"; \ @@ -114,9 +146,10 @@ install: all ## A dedicated test asserts that this is always true. uninstall: rm -rf \ - '$(DESTDIR)$(BINDIR)'/$(NAME) \ - '$(DESTDIR)$(DATADIR)' \ - '$(DESTDIR)$(SRCDIR)' \ + '$(DESTDIR)$(BINDIR)'/$(NAME) \ + '$(DESTDIR)$(GOLIBDIR)'/$(NAME).a \ + '$(DESTDIR)$(DATADIR)' \ + '$(DESTDIR)$(SRCDIR)' \ run-papod: $(NAME).bin diff --git a/src/cmd/main.go b/src/cmd/main.go deleted file mode 100644 index 8e80599..0000000 --- a/src/cmd/main.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -import "euandre.org/papod/src" - -func main() { - papod.Main() -} diff --git a/src/lib.go b/src/lib.go deleted file mode 100644 index b06829a..0000000 --- a/src/lib.go +++ /dev/null @@ -1,582 +0,0 @@ -package papod - -import ( - "bufio" - "bytes" - "database/sql" - "errors" - "flag" - "fmt" - "io" - "io/ioutil" - "log/slog" - "net" - "os" - "regexp" - "runtime/debug" - "sort" - "strings" - "sync" - "time" - - g "euandre.org/gobang/src" - - _ "github.com/mattn/go-sqlite3" -) - - - - - -// Global variables -var ( - Version string - Colour string -) - - - -func SetEnvironmentVariables() { - Version = os.Getenv("PAPOD_VERSION") - if Version == "" { - Version = "PAPOD-VERSION-UNKNOWN" - } - - Colour = os.Getenv("PAPOD_COLOUR") - if Colour == "" { - Colour = "PAPOD-COLOUR-UNKNOWN" - } -} - - -// FIXME: reorder -var EmitActiveConnection = g.MakeGauge("active-connections") -var EmitNicksInChannel = g.MakeGauge("nicks-in-channel") -var EmitReceivedMessage = g.MakeCounter("received-message") -var EmitWriteToClientError = g.MakeCounter("write-to-client") - -const pingFrequency = time.Duration(30) * time.Second -const pongMaxLatency = time.Duration(5) * time.Second - -// type UUID string - -type Channel struct { -} - -type Connection struct { - conn net.Conn - replyChan chan string - lastReadFrom time.Time - lastWrittenTo time.Time - // id *UUID - id string - isAuthenticated bool -} - -type User struct { - connections []Connection -} - -type State struct { - users map[string]*User -} - -type Context struct { - db *sql.DB - state State - tx chan int -} - -type MessageParams struct { - Middle []string - Trailing string -} - -type Message struct { - Prefix string - Command string - Params MessageParams - Raw string -} - -var ( - CmdUSER = Message { Command: "USER" } - CmdPRIVMSG = Message { Command: "PRIVMSG" } - CmdJOIN = Message { Command: "JOIN" } -) - -func SplitOnCRLF(data []byte, _atEOF bool) (int, []byte, error) { - idx := bytes.Index(data, []byte { '\r', '\n' }) - if idx == -1 { - return 0, nil, nil - } - - return idx + 2, data[0:idx], nil -} - -func SplitOnRawMessage(data []byte, atEOF bool) (int, []byte, error) { - advance, token, error := SplitOnCRLF(data, atEOF) - - if len(token) == 0 { - return advance, nil, error - } - - return advance, token, error -} - -func SplitSpaces(r rune) bool { - return r == ' ' -} - -func ParseMessageParams(params string) MessageParams { - const sep = " :" - - var middle string - var trailing string - - idx := strings.Index(params, sep) - if idx == -1 { - middle = params - trailing = "" - } else { - middle = params[:idx] - trailing = params[idx + len(sep):] - } - - return MessageParams { - Middle: strings.FieldsFunc(middle, SplitSpaces), - Trailing: trailing, - } -} - -var MessageRegex = regexp.MustCompilePOSIX( - // - //1 2 3 4 - `^(:([^ ]+) +)?([a-zA-Z]+) *( .*)$`, -) -func ParseMessage(rawMessage string) (Message, error) { - var msg Message - - components := MessageRegex.FindStringSubmatch(rawMessage) - if components == nil { - return msg, errors.New("Can't parse message") - } - - msg = Message { - Prefix: components[2], - Command: components[3], - Params: ParseMessageParams(components[4]), - Raw: rawMessage, - } - return msg, nil -} - -func HandleUnknown(ctx *Context, msg Message) { - g.Warning( - "Unsupported command", "unsupported-command", - "command", msg.Command, - ) - var r Reply = ReplyUnknown - r.Prefix = "dunno" - // return []Action { r } -} - -func HandleUSER(ctx *Context, msg Message) { - fmt.Printf("USER: %#v\n", msg) -} - -func HandlePRIVMSG(ctx *Context, msg Message) { - // . assert no missing params - // . write to DB: (after auth) - // . channel timeline: message from $USER - // . reply to $USER - // . broadcast new timeline event to members of the channel - - stmt, err := ctx.db.Prepare(` - INSERT INTO messages - (id, sender_id, body, timestamp) - VALUES - (?, ?, ?, ? ); - `) - if err != nil { - // FIXME: reply error - fmt.Println("can't prepare: ", err) - return - } - defer stmt.Close() - - ret, err := stmt.Exec( - g.NewUUID().ToString(), - "FIXME", - "FIXME", - time.Now(), - ) - if err != nil { - // FIXME: reply error - fmt.Println("xablau can't prepare: ", err) - return - } - - fmt.Println("ret: ", ret) -} - -func HandleJOIN(ctx *Context, msg Message) { - fmt.Printf("JOIN: %#v\n", msg) - - // . write to DB: (after auth) - // . $USER now in channel - // . channel timeline: $USER joined - // . reply to $USER - // . broadcast new timeline event to members of the channel -} - -func ReplyAnonymous() { -} - -func PersistMessage(msg Message) { -} - -type ActionType int -const ( - ActionReply = iota -) - -type Action interface { - Type() ActionType -} - -type Reply struct { - Prefix string - Command int - Params MessageParams -} - -func (reply Reply) Type() ActionType { - return ActionReply -} - -var ( - ReplyUnknown = Reply { - Command: 421, - Params: MessageParams { - Middle: []string { }, - Trailing: "Unknown command", - }, - } -) - -var Commands = map[string]func(*Context, Message) { - CmdUSER.Command: HandleUSER, - CmdPRIVMSG.Command: HandlePRIVMSG, - CmdJOIN.Command: HandleJOIN, -} - -func ActionFnFor(command string) func(*Context, Message) { - fn := Commands[command] - if fn != nil { - return fn - } - - return HandleUnknown -} - -func ProcessMessage(ctx *Context, connection *Connection, rawMessage string) { - connection.lastReadFrom = time.Now() - - msg, err := ParseMessage(rawMessage) - if err != nil { - g.Info( - "Error processing message", - "process-message", - "err", err, - ) - return - } - - if msg.Command == CmdUSER.Command { - args := msg.Params.Middle - if len(args) == 0 { - go ReplyAnonymous() - return - } - connection.id = args[0] - connection.isAuthenticated = true - } - - if !connection.isAuthenticated { - go ReplyAnonymous() - return - } - - ActionFnFor(msg.Command)(ctx, msg) -} - -func ReadLoop(ctx *Context, connection *Connection) { - scanner := bufio.NewScanner(connection.conn) - scanner.Split(SplitOnRawMessage) - for scanner.Scan() { - ProcessMessage(ctx, connection, scanner.Text()) - } -} - -func WriteLoop(ctx *Context, connection *Connection) { - for message := range connection.replyChan { - _, err := io.WriteString(connection.conn, message) - if err != nil { - g.Error( - "Failed to send data to user", - "user-reply-error", - "err", err, - ) - EmitWriteToClientError() - continue - } - - connection.lastWrittenTo = time.Now() - } - - EmitActiveConnection.Dec() - connection.conn.Close() -} - -func Kill(ctx *Context, connection *Connection) { - // lock? - delete(ctx.state.users, connection.id) - // unlock? - close(connection.replyChan) - connection.conn.Close() // Ignore errors? -} - -const PingWindow = 30 * time.Second -func PingLoop(ctx *Context, connection *Connection) { - for { - time.Sleep(PingWindow) - if (time.Since(connection.lastReadFrom) <= PingWindow) { - continue - } - window := connection.lastWrittenTo.Sub(connection.lastReadFrom) - if (window <= PingWindow) { - connection.replyChan <- "PING" - continue - } - - Kill(ctx, connection) - break - } -} - -func HandleConnection(ctx *Context, conn net.Conn) { - EmitActiveConnection.Inc() - // FIXME: WaitGroup here? - now := time.Now() - connection := Connection { - conn: conn, - isAuthenticated: false, - lastReadFrom: now, - lastWrittenTo: now, - } - go ReadLoop(ctx, &connection) - go WriteLoop(ctx, &connection) - go PingLoop(ctx, &connection) -} - -func IRCdLoop(ctx *Context, publicSocketPath string) { - listener, err := net.Listen("unix", publicSocketPath) - g.FatalIf(err) - g.Info("IRCd started", "component-up", "component", "ircd") - - for { - conn, err := listener.Accept() - if err != nil { - g.Warning( - "Error accepting a public IRCd connection", - "accept-connection", - "err", err, - ) - // conn.Close() // FIXME: is conn nil? - continue - } - // FIXME: where does it get closed - go HandleConnection(ctx, conn) - } -} - -func CommandListenerLoop(ctx *Context, commandSocketPath string) { - listener, err := net.Listen("unix", commandSocketPath) - g.FatalIf(err) - g.Info( - "command listener started", - "component-up", - "component", "command-listener", - ) - - for { - conn, err := listener.Accept() - if err != nil { - g.Warning( - "Error accepting a command connection", - "accept-command", - "err", err, - ) - continue - } - defer conn.Close() - - // TODO: handle commands - } -} - -func TransactorLoop(ctx *Context) { - g.Info("transactor started", "component-up", "component", "transactor") - EmitActiveConnection.Inc() - - for tx := range ctx.tx { - fmt.Println(tx) - } -} - -func InitMigrations(db *sql.DB) { - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS migrations ( - filename TEXT PRIMARY KEY - ); - `) - g.FatalIf(err) -} - -const MIGRATIONS_DIR = "src/sql/migrations/" -func PendingMigrations(db *sql.DB) []string { - files, err := ioutil.ReadDir(MIGRATIONS_DIR) - g.FatalIf(err) - - set := make(map[string]bool) - for _, file := range files { - set[file.Name()] = true - } - - rows, err := db.Query(`SELECT filename FROM migrations;`) - g.FatalIf(err) - defer rows.Close() - - for rows.Next() { - var filename string - err := rows.Scan(&filename) - g.FatalIf(err) - delete(set, filename) - } - g.FatalIf(rows.Err()) - - difference := make([]string, 0) - for filename := range set { - difference = append(difference, filename) - } - - sort.Sort(sort.StringSlice(difference)) - return difference -} - -func RunMigrations(db *sql.DB) { - InitMigrations(db) - - stmt, err := db.Prepare(`INSERT INTO migrations (filename) VALUES (?);`) - g.FatalIf(err) - defer stmt.Close() - - for _, filename := range PendingMigrations(db) { - g.Info("Running migration file", "exec-migration-file", - "filename", filename, - ) - - tx, err := db.Begin() - g.FatalIf(err) - - sql, err := os.ReadFile(MIGRATIONS_DIR + filename) - g.FatalIf(err) - - _, err = tx.Exec(string(sql)) - g.FatalIf(err) - - _, err = tx.Stmt(stmt).Exec(filename) - g.FatalIf(err) - - err = tx.Commit() - g.FatalIf(err) - } -} - -func InitDB(databasePath string) *sql.DB { - db, err := sql.Open("sqlite3", databasePath) - g.FatalIf(err) - RunMigrations(db) - return db -} - -func Init() { - g.Init() - SetEnvironmentVariables() -} - -func Start(ctx *Context, publicSocketPath string, commandSocketPath string) { - buildInfo, ok := debug.ReadBuildInfo() - if !ok { - g.Fatal(errors.New("error on debug.ReadBuildInfo()")) - } - - g.Info("-", "lifecycle-event", - "event", "starting-server", - slog.Group( - "go", - "version", buildInfo.GoVersion, - "settings", buildInfo.Settings, - "deps", buildInfo.Deps, - ), - ) - - var wg sync.WaitGroup - bgRun := func(f func()) { - wg.Add(1) - go func() { - f() - wg.Done() - }() - } - bgRun(func() { IRCdLoop(ctx, publicSocketPath) }) - bgRun(func() { CommandListenerLoop(ctx, commandSocketPath) }) - bgRun(func() { TransactorLoop(ctx) }) - wg.Wait() -} - -func BuildContext(databasePath string) *Context { - db := InitDB(databasePath) - tx := make(chan int, 100) - return &Context { - db: db, - tx: tx, - } -} - -var ( - databasePath = flag.String( - "f", - "papod.db", - "The path to the database file", - ) - publicSocketPath = flag.String( - "s", - "papod.public.socket", - "The path to the socket that handles the public traffic", - ) - commandSocketPath = flag.String( - "S", - "papod.command.socket", - "The path to the private IPC commands socket", - ) -) - - -func Main() { - Init() - flag.Parse() - ctx := BuildContext(*databasePath) - Start(ctx, *publicSocketPath, *commandSocketPath) -} diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..b591f5c --- /dev/null +++ b/src/main.go @@ -0,0 +1,7 @@ +package main + +import "papod" + +func main() { + papod.Main() +} diff --git a/src/papod.go b/src/papod.go new file mode 100644 index 0000000..91e1145 --- /dev/null +++ b/src/papod.go @@ -0,0 +1,583 @@ +package papod + +import ( + "bufio" + "bytes" + "database/sql" + "errors" + "flag" + "fmt" + "io" + "io/ioutil" + "net" + "os" + "regexp" + "sort" + "strings" + "sync" + "time" + + g "gobang" + _ "golite" +) + + + + + +// Global variables +var ( + Version string + Colour string +) + + + +func SetEnvironmentVariables() { + Version = os.Getenv("PAPOD_VERSION") + if Version == "" { + Version = "PAPOD-VERSION-UNKNOWN" + } + + Colour = os.Getenv("PAPOD_COLOUR") + if Colour == "" { + Colour = "PAPOD-COLOUR-UNKNOWN" + } +} + + +// FIXME: reorder +var EmitActiveConnection = g.MakeGauge("active-connections") +var EmitNicksInChannel = g.MakeGauge("nicks-in-channel") +var EmitReceivedMessage = g.MakeCounter("received-message") +var EmitWriteToClientError = g.MakeCounter("write-to-client") + +const pingFrequency = time.Duration(30) * time.Second +const pongMaxLatency = time.Duration(5) * time.Second + +// type UUID string + +type Channel struct { +} + +type Connection struct { + conn net.Conn + replyChan chan string + lastReadFrom time.Time + lastWrittenTo time.Time + // id *UUID + id string + isAuthenticated bool +} + +type User struct { + connections []Connection +} + +type State struct { + users map[string]*User +} + +type Context struct { + db *sql.DB + state State + tx chan int +} + +type MessageParams struct { + Middle []string + Trailing string +} + +type Message struct { + Prefix string + Command string + Params MessageParams + Raw string +} + +var ( + CmdUSER = Message { Command: "USER" } + CmdPRIVMSG = Message { Command: "PRIVMSG" } + CmdJOIN = Message { Command: "JOIN" } +) + +func SplitOnCRLF(data []byte, _atEOF bool) (int, []byte, error) { + idx := bytes.Index(data, []byte { '\r', '\n' }) + if idx == -1 { + return 0, nil, nil + } + + return idx + 2, data[0:idx], nil +} + +func SplitOnRawMessage(data []byte, atEOF bool) (int, []byte, error) { + advance, token, error := SplitOnCRLF(data, atEOF) + + if len(token) == 0 { + return advance, nil, error + } + + return advance, token, error +} + +func SplitSpaces(r rune) bool { + return r == ' ' +} + +func ParseMessageParams(params string) MessageParams { + const sep = " :" + + var middle string + var trailing string + + idx := strings.Index(params, sep) + if idx == -1 { + middle = params + trailing = "" + } else { + middle = params[:idx] + trailing = params[idx + len(sep):] + } + + return MessageParams { + Middle: strings.FieldsFunc(middle, SplitSpaces), + Trailing: trailing, + } +} + +var MessageRegex = regexp.MustCompilePOSIX( + // + //1 2 3 4 + `^(:([^ ]+) +)?([a-zA-Z]+) *( .*)$`, +) +func ParseMessage(rawMessage string) (Message, error) { + var msg Message + + components := MessageRegex.FindStringSubmatch(rawMessage) + if components == nil { + return msg, errors.New("Can't parse message") + } + + msg = Message { + Prefix: components[2], + Command: components[3], + Params: ParseMessageParams(components[4]), + Raw: rawMessage, + } + return msg, nil +} + +func HandleUnknown(ctx *Context, msg Message) { + g.Warning( + "Unsupported command", "unsupported-command", + "command", msg.Command, + ) + var r Reply = ReplyUnknown + r.Prefix = "dunno" + // return []Action { r } +} + +func HandleUSER(ctx *Context, msg Message) { + fmt.Printf("USER: %#v\n", msg) +} + +func HandlePRIVMSG(ctx *Context, msg Message) { + // . assert no missing params + // . write to DB: (after auth) + // . channel timeline: message from $USER + // . reply to $USER + // . broadcast new timeline event to members of the channel + + stmt, err := ctx.db.Prepare(` + INSERT INTO messages + (id, sender_id, body, timestamp) + VALUES + (?, ?, ?, ? ); + `) + if err != nil { + // FIXME: reply error + fmt.Println("can't prepare: ", err) + return + } + defer stmt.Close() + + ret, err := stmt.Exec( + g.NewUUID().String(), + "FIXME", + "FIXME", + time.Now(), + ) + if err != nil { + // FIXME: reply error + fmt.Println("xablau can't prepare: ", err) + return + } + + fmt.Println("ret: ", ret) +} + +func HandleJOIN(ctx *Context, msg Message) { + fmt.Printf("JOIN: %#v\n", msg) + + // . write to DB: (after auth) + // . $USER now in channel + // . channel timeline: $USER joined + // . reply to $USER + // . broadcast new timeline event to members of the channel +} + +func ReplyAnonymous() { +} + +func PersistMessage(msg Message) { +} + +type ActionType int +const ( + ActionReply = iota +) + +type Action interface { + Type() ActionType +} + +type Reply struct { + Prefix string + Command int + Params MessageParams +} + +func (reply Reply) Type() ActionType { + return ActionReply +} + +var ( + ReplyUnknown = Reply { + Command: 421, + Params: MessageParams { + Middle: []string { }, + Trailing: "Unknown command", + }, + } +) + +var Commands = map[string]func(*Context, Message) { + CmdUSER.Command: HandleUSER, + CmdPRIVMSG.Command: HandlePRIVMSG, + CmdJOIN.Command: HandleJOIN, +} + +func ActionFnFor(command string) func(*Context, Message) { + fn := Commands[command] + if fn != nil { + return fn + } + + return HandleUnknown +} + +func ProcessMessage(ctx *Context, connection *Connection, rawMessage string) { + connection.lastReadFrom = time.Now() + + msg, err := ParseMessage(rawMessage) + if err != nil { + g.Info( + "Error processing message", + "process-message", + "err", err, + ) + return + } + + if msg.Command == CmdUSER.Command { + args := msg.Params.Middle + if len(args) == 0 { + go ReplyAnonymous() + return + } + connection.id = args[0] + connection.isAuthenticated = true + } + + if !connection.isAuthenticated { + go ReplyAnonymous() + return + } + + ActionFnFor(msg.Command)(ctx, msg) +} + +func ReadLoop(ctx *Context, connection *Connection) { + scanner := bufio.NewScanner(connection.conn) + scanner.Split(SplitOnRawMessage) + for scanner.Scan() { + ProcessMessage(ctx, connection, scanner.Text()) + } +} + +func WriteLoop(ctx *Context, connection *Connection) { + for message := range connection.replyChan { + _, err := io.WriteString(connection.conn, message) + if err != nil { + g.Error( + "Failed to send data to user", + "user-reply-error", + "err", err, + ) + EmitWriteToClientError() + continue + } + + connection.lastWrittenTo = time.Now() + } + + EmitActiveConnection.Dec() + connection.conn.Close() +} + +func Kill(ctx *Context, connection *Connection) { + // lock? + delete(ctx.state.users, connection.id) + // unlock? + close(connection.replyChan) + connection.conn.Close() // Ignore errors? +} + +const PingWindow = 30 * time.Second +func PingLoop(ctx *Context, connection *Connection) { + for { + time.Sleep(PingWindow) + if (time.Since(connection.lastReadFrom) <= PingWindow) { + continue + } + window := connection.lastWrittenTo.Sub(connection.lastReadFrom) + if (window <= PingWindow) { + connection.replyChan <- "PING" + continue + } + + Kill(ctx, connection) + break + } +} + +func HandleConnection(ctx *Context, conn net.Conn) { + EmitActiveConnection.Inc() + // FIXME: WaitGroup here? + now := time.Now() + connection := Connection { + conn: conn, + isAuthenticated: false, + lastReadFrom: now, + lastWrittenTo: now, + } + go ReadLoop(ctx, &connection) + go WriteLoop(ctx, &connection) + go PingLoop(ctx, &connection) +} + +func IRCdLoop(ctx *Context, publicSocketPath string) { + listener, err := net.Listen("unix", publicSocketPath) + g.FatalIf(err) + g.Info("IRCd started", "component-up", "component", "ircd") + + for { + conn, err := listener.Accept() + if err != nil { + g.Warning( + "Error accepting a public IRCd connection", + "accept-connection", + "err", err, + ) + // conn.Close() // FIXME: is conn nil? + continue + } + // FIXME: where does it get closed + go HandleConnection(ctx, conn) + } +} + +func CommandListenerLoop(ctx *Context, commandSocketPath string) { + listener, err := net.Listen("unix", commandSocketPath) + g.FatalIf(err) + g.Info( + "command listener started", + "component-up", + "component", "command-listener", + ) + + for { + conn, err := listener.Accept() + if err != nil { + g.Warning( + "Error accepting a command connection", + "accept-command", + "err", err, + ) + continue + } + defer conn.Close() + + // TODO: handle commands + } +} + +func TransactorLoop(ctx *Context) { + g.Info("transactor started", "component-up", "component", "transactor") + EmitActiveConnection.Inc() + + for tx := range ctx.tx { + fmt.Println(tx) + } +} + +func InitMigrations(db *sql.DB) { + _, err := db.Exec(` + CREATE TABLE IF NOT EXISTS migrations ( + filename TEXT PRIMARY KEY + ); + `) + g.FatalIf(err) +} + +const MIGRATIONS_DIR = "src/sql/migrations/" +func PendingMigrations(db *sql.DB) []string { + files, err := ioutil.ReadDir(MIGRATIONS_DIR) + g.FatalIf(err) + + set := make(map[string]bool) + for _, file := range files { + set[file.Name()] = true + } + + rows, err := db.Query(`SELECT filename FROM migrations;`) + g.FatalIf(err) + defer rows.Close() + + for rows.Next() { + var filename string + err := rows.Scan(&filename) + g.FatalIf(err) + delete(set, filename) + } + g.FatalIf(rows.Err()) + + difference := make([]string, 0) + for filename := range set { + difference = append(difference, filename) + } + + sort.Sort(sort.StringSlice(difference)) + return difference +} + +func RunMigrations(db *sql.DB) { + InitMigrations(db) + + stmt, err := db.Prepare(`INSERT INTO migrations (filename) VALUES (?);`) + g.FatalIf(err) + defer stmt.Close() + + for _, filename := range PendingMigrations(db) { + g.Info("Running migration file", "exec-migration-file", + "filename", filename, + ) + + tx, err := db.Begin() + g.FatalIf(err) + + sql, err := os.ReadFile(MIGRATIONS_DIR + filename) + g.FatalIf(err) + + _, err = tx.Exec(string(sql)) + g.FatalIf(err) + + _, err = tx.Stmt(stmt).Exec(filename) + g.FatalIf(err) + + err = tx.Commit() + g.FatalIf(err) + } +} + +func InitDB(databasePath string) *sql.DB { + db, err := sql.Open("sqlite3", databasePath) + g.FatalIf(err) + RunMigrations(db) + return db +} + +func Init() { + g.Init() + SetEnvironmentVariables() +} + +func Start(ctx *Context, publicSocketPath string, commandSocketPath string) { + /* + buildInfo, ok := debug.ReadBuildInfo() + if !ok { + g.Fatal(errors.New("error on debug.ReadBuildInfo()")) + } + */ + + g.Info("-", "lifecycle-event", + "event", "starting-server", + /* + slog.Group( + "go", + "version", buildInfo.GoVersion, + "settings", buildInfo.Settings, + "deps", buildInfo.Deps, + ), + */ + ) + + var wg sync.WaitGroup + bgRun := func(f func()) { + wg.Add(1) + go func() { + f() + wg.Done() + }() + } + bgRun(func() { IRCdLoop(ctx, publicSocketPath) }) + bgRun(func() { CommandListenerLoop(ctx, commandSocketPath) }) + bgRun(func() { TransactorLoop(ctx) }) + wg.Wait() +} + +func BuildContext(databasePath string) *Context { + db := InitDB(databasePath) + tx := make(chan int, 100) + return &Context { + db: db, + tx: tx, + } +} + +var ( + databasePath = flag.String( + "f", + "papod.db", + "The path to the database file", + ) + publicSocketPath = flag.String( + "s", + "papod.public.socket", + "The path to the socket that handles the public traffic", + ) + commandSocketPath = flag.String( + "S", + "papod.command.socket", + "The path to the private IPC commands socket", + ) +) + + +func Main() { + Init() + flag.Parse() + ctx := BuildContext(*databasePath) + Start(ctx, *publicSocketPath, *commandSocketPath) +} diff --git a/tests/integration.sh b/tests/integration.sh new file mode 100755 index 0000000..fcb62ca --- /dev/null +++ b/tests/integration.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -eu + +exit diff --git a/tests/lib_test.go b/tests/lib_test.go deleted file mode 100644 index 67fbb40..0000000 --- a/tests/lib_test.go +++ /dev/null @@ -1,514 +0,0 @@ -package papod_test - -import ( - "bufio" - "database/sql" - "errors" - "reflect" - "strings" - "testing" - - g "euandre.org/gobang/src" - - "euandre.org/papod/src" -) - - -func errorIf(t *testing.T, err error) { - if err != nil { - t.Errorf("Unexpected error: %#v\n", err) - } -} - -func errorIfNotI(t *testing.T, i int, err error) { - if err == nil { - t.Errorf("Expected error, got nil (i = %d)\n", i) - } -} - -func assertEqualI(t *testing.T, i int, given any, expected any) { - if !reflect.DeepEqual(given, expected) { - t.Errorf("given != expected (i = %d)\n", i) - t.Errorf("given: %#v\n", given) - t.Errorf("expected: %#v\n", expected) - } -} - -func assertEqual(t *testing.T, given any, expected any) { - if !reflect.DeepEqual(given, expected) { - t.Errorf("given != expected") - t.Errorf("given: %#v\n", given) - t.Errorf("expected: %#v\n", expected) - } -} - - -func TestSplitOnCRLF(t *testing.T) { - type tableT struct { - input string - expected []string - } - table := []tableT { - { - "", - nil, - }, - { - "\r\n", - []string { "" }, - }, - { - "abc\r\n", - []string { "abc" }, - }, - { - "abc\r\n ", - []string { "abc" }, - }, - { - "abc\r\n \r\n", - []string { "abc", " " }, - }, - { - " \r\n \r\n", - []string { " ", " " }, - }, - { - "aaa\r\nbbb\r\nccc\r\n", - []string { "aaa", "bbb", "ccc" }, - }, - { - "\r\nsplit \r \n CRLF\r\n\r\n", - []string { "", "split \r \n CRLF", "" }, - }, - } - - for i, entry := range table { - var given []string - scanner := bufio.NewScanner(strings.NewReader(entry.input)) - scanner.Split(papod.SplitOnCRLF) - for scanner.Scan() { - given = append(given, scanner.Text()) - } - - err := scanner.Err() - errorIf(t, err) - assertEqualI(t, i, given, entry.expected) - } -} - -func TestSplitOnRawMessage(t *testing.T) { - type tableT struct { - input string - expected []string - } - table := []tableT { - { - "first message\r\nsecond message\r\n", - []string { "first message", "second message" }, - }, - { - "message 1\r\n\r\nmessage 2\r\n\r\nignored", - []string { "message 1", "message 2" }, - }, - } - - - for i, entry := range table { - var given []string - scanner := bufio.NewScanner(strings.NewReader(entry.input)) - scanner.Split(papod.SplitOnRawMessage) - for scanner.Scan() { - given = append(given, scanner.Text()) - } - - err := scanner.Err() - errorIf(t, err) - assertEqualI(t, i, given, entry.expected) - } -} - -func TestParseMessageParams(t *testing.T) { - type tableT struct { - input string - expected papod.MessageParams - } - table := []tableT { - { - "", - papod.MessageParams { - Middle: []string { }, - Trailing: "", - }, - }, - { - " ", - papod.MessageParams { - Middle: []string { }, - Trailing: "", - }, - }, - { - " :", - papod.MessageParams { - Middle: []string { }, - Trailing: "", - }, - }, - { - " : ", - papod.MessageParams { - Middle: []string { }, - Trailing: " ", - }, - }, - { - ": ", - papod.MessageParams { - Middle: []string { ":" }, - Trailing: "", - }, - }, - { - ": ", - papod.MessageParams { - Middle: []string { ":" }, - Trailing: "", - }, - }, - { - " : ", - papod.MessageParams { - Middle: []string { }, - Trailing: " ", - }, - }, - { - " :", - papod.MessageParams { - Middle: []string { }, - Trailing: "", - }, - }, - { - " :", - papod.MessageParams { - Middle: []string { }, - Trailing: "", - }, - }, - { - "a", - papod.MessageParams { - Middle: []string { "a" }, - Trailing: "", - }, - }, - { - "ab", - papod.MessageParams { - Middle: []string { "ab" }, - Trailing: "", - }, - }, - { - "a b", - papod.MessageParams { - Middle: []string { "a", "b" }, - Trailing: "", - }, - }, - { - "a b c", - papod.MessageParams { - Middle: []string { "a", "b", "c" }, - Trailing: "", - }, - }, - { - "a b:c", - papod.MessageParams { - Middle: []string { "a", "b:c" }, - Trailing: "", - }, - }, - { - "a b:c:", - papod.MessageParams { - Middle: []string { "a", "b:c:" }, - Trailing: "", - }, - }, - { - "a b :c", - papod.MessageParams { - Middle: []string { "a", "b" }, - Trailing: "c", - }, - }, - { - "a b :c:", - papod.MessageParams { - Middle: []string { "a", "b" }, - Trailing: "c:", - }, - }, - { - "a b :c ", - papod.MessageParams { - Middle: []string { "a", "b" }, - Trailing: "c ", - }, - }, - { - "a b : c", - papod.MessageParams { - Middle: []string { "a", "b" }, - Trailing: " c", - }, - }, - { - "a b : c ", - papod.MessageParams { - Middle: []string { "a", "b" }, - Trailing: " c ", - }, - }, - { - "a b : c :", - papod.MessageParams { - Middle: []string { "a", "b" }, - Trailing: " c :", - }, - }, - { - "a b : c : ", - papod.MessageParams { - Middle: []string { "a", "b" }, - Trailing: " c : ", - }, - }, - { - "a b : c :d", - papod.MessageParams { - Middle: []string { "a", "b" }, - Trailing: " c :d", - }, - }, - { - "a b : c :d ", - papod.MessageParams { - Middle: []string { "a", "b" }, - Trailing: " c :d ", - }, - }, - { - "a b : c : d ", - papod.MessageParams { - Middle: []string { "a", "b" }, - Trailing: " c : d ", - }, - }, - } - - for i, entry := range table { - given := papod.ParseMessageParams(entry.input) - assertEqualI(t, i, given, entry.expected) - } -} - -func TestParseMessage(t *testing.T) { - type tableTOK struct { - input string - expected papod.Message - } - tableOK := []tableTOK {{ - "NICK joebloe ", - papod.Message { - Prefix: "", - Command: "NICK", - Params: papod.MessageParams { - Middle: []string { "joebloe" }, - Trailing: "", - }, - Raw: "NICK joebloe ", - }, - }, { - "USER joebloe 0.0.0.0 joe :Joe Bloe", - papod.Message { - Prefix: "", - Command: "USER", - Params: papod.MessageParams { - Middle: []string { - "joebloe", "0.0.0.0", "joe", - }, - Trailing: "Joe Bloe", - }, - Raw: "USER joebloe 0.0.0.0 joe :Joe Bloe", - }, - }, { - ":pre USER joebloe 0.0.0.0 joe :Joe Bloe", - papod.Message { - Prefix: "pre", - Command: "USER", - Params: papod.MessageParams { - Middle: []string { - "joebloe", "0.0.0.0", "joe", - }, - Trailing: "Joe Bloe", - }, - Raw: ":pre USER joebloe 0.0.0.0 joe :Joe Bloe", - }, - }, { - ":pre USER joebloe 0.0.0.0 joe : Joe Bloe ", - papod.Message { - Prefix: "pre", - Command: "USER", - Params: papod.MessageParams { - Middle: []string { - "joebloe", "0.0.0.0", "joe", - }, - Trailing: " Joe Bloe ", - }, - Raw: ":pre USER joebloe 0.0.0.0 joe : Joe Bloe ", - }, - }, { - ":pre USER joebloe: 0:0:0:1 joe::a: : Joe Bloe ", - papod.Message { - Prefix: "pre", - Command: "USER", - Params: papod.MessageParams { - Middle: []string { - "joebloe:", "0:0:0:1", "joe::a:", - }, - Trailing: " Joe Bloe ", - }, - Raw: ":pre USER joebloe: 0:0:0:1 joe::a: : Joe Bloe ", - }, - }, { - ":pre USER :Joe Bloe", - papod.Message { - Prefix: "pre", - Command: "USER", - Params: papod.MessageParams { - Middle: []string { }, - Trailing: "Joe Bloe", - }, - Raw: ":pre USER :Joe Bloe", - }, - }, { - ":pre USER : Joe Bloe", - papod.Message { - Prefix: "pre", - Command: "USER", - Params: papod.MessageParams { - Middle: []string { }, - Trailing: " Joe Bloe", - }, - Raw: ":pre USER : Joe Bloe", - }, - }, { - ":pre USER : Joe Bloe", - papod.Message { - Prefix: "pre", - Command: "USER", - Params: papod.MessageParams { - Middle: []string { }, - Trailing: " Joe Bloe", - }, - Raw: ":pre USER : Joe Bloe", - }, - }, { - ":pre USER : ", - papod.Message { - Prefix: "pre", - Command: "USER", - Params: papod.MessageParams { - Middle: []string { }, - Trailing: " ", - }, - Raw: ":pre USER : ", - }, - }, { - ":pre USER :", - papod.Message { - Prefix: "pre", - Command: "USER", - Params: papod.MessageParams { - Middle: []string { }, - Trailing: "", - }, - Raw: ":pre USER :", - }, - }} - - for i, entry := range tableOK { - given, err := papod.ParseMessage(entry.input) - errorIf(t, err) - assertEqualI(t, i, given, entry.expected) - } - - - type tableErrorT struct { - input string - expected error - } - parseErr := errors.New("Can't parse message") - tableError := []tableErrorT { - { - ":pre", - parseErr, - }, - { - ": pre", - parseErr, - }, - { - ":pre N1CK", - parseErr, - }, - } - - for i, entry := range tableError { - _, given := papod.ParseMessage(entry.input) - assertEqualI(t, i, given, entry.expected) - } -} - -func TestInitMigrations(t *testing.T) { - const query = `SELECT filename FROM migrations;` - - db, err := sql.Open("sqlite3", ":memory:") - g.FatalIf(err) - - _, err = db.Query(query) - assertEqual(t, err.Error(), "no such table: migrations") - - for i := 0; i < 5; i++ { - papod.InitMigrations(db) - rows, err := db.Query(query) - g.FatalIf(err) - assertEqual(t, rows.Next(), false) - g.FatalIf(rows.Err()) - } -} - -func TestPendingMigrations(t *testing.T) { - db, err := sql.Open("sqlite3", ":memory:") - g.FatalIf(err) - - papod.InitMigrations(db) - pending1 := papod.PendingMigrations(db) - pending2 := papod.PendingMigrations(db) - - assertEqual(t, pending1, pending2) -} - -func TestRunMigrations(t *testing.T) { - db, err := sql.Open("sqlite3", ":memory:") - g.FatalIf(err) - - for i := 0; i < 5; i++ { - papod.RunMigrations(db) - } -} diff --git a/tests/main.go b/tests/main.go new file mode 100644 index 0000000..f32854e --- /dev/null +++ b/tests/main.go @@ -0,0 +1,7 @@ +package main + +import "papod" + +func main() { + papod.MainTest() +} diff --git a/tests/papod.go b/tests/papod.go new file mode 100644 index 0000000..af2a5f7 --- /dev/null +++ b/tests/papod.go @@ -0,0 +1,540 @@ +package papod + +import ( + "bufio" + "database/sql" + "errors" + "os" + "reflect" + "strings" + "testing" + "testing/internal/testdeps" + + g "gobang" +) + + +func errorIf(t *testing.T, err error) { + if err != nil { + t.Errorf("Unexpected error: %#v\n", err) + } +} + +func errorIfNotI(t *testing.T, i int, err error) { + if err == nil { + t.Errorf("Expected error, got nil (i = %d)\n", i) + } +} + +func assertEqualI(t *testing.T, i int, given any, expected any) { + if !reflect.DeepEqual(given, expected) { + t.Errorf("given != expected (i = %d)\n", i) + t.Errorf("given: %#v\n", given) + t.Errorf("expected: %#v\n", expected) + } +} + +func assertEqual(t *testing.T, given any, expected any) { + if !reflect.DeepEqual(given, expected) { + t.Errorf("given != expected") + t.Errorf("given: %#v\n", given) + t.Errorf("expected: %#v\n", expected) + } +} + + +func TestSplitOnCRLF(t *testing.T) { + type tableT struct { + input string + expected []string + } + table := []tableT { + { + "", + nil, + }, + { + "\r\n", + []string { "" }, + }, + { + "abc\r\n", + []string { "abc" }, + }, + { + "abc\r\n ", + []string { "abc" }, + }, + { + "abc\r\n \r\n", + []string { "abc", " " }, + }, + { + " \r\n \r\n", + []string { " ", " " }, + }, + { + "aaa\r\nbbb\r\nccc\r\n", + []string { "aaa", "bbb", "ccc" }, + }, + { + "\r\nsplit \r \n CRLF\r\n\r\n", + []string { "", "split \r \n CRLF", "" }, + }, + } + + for i, entry := range table { + var given []string + scanner := bufio.NewScanner(strings.NewReader(entry.input)) + scanner.Split(SplitOnCRLF) + for scanner.Scan() { + given = append(given, scanner.Text()) + } + + err := scanner.Err() + errorIf(t, err) + assertEqualI(t, i, given, entry.expected) + } +} + +func TestSplitOnRawMessage(t *testing.T) { + type tableT struct { + input string + expected []string + } + table := []tableT { + { + "first message\r\nsecond message\r\n", + []string { "first message", "second message" }, + }, + { + "message 1\r\n\r\nmessage 2\r\n\r\nignored", + []string { "message 1", "message 2" }, + }, + } + + + for i, entry := range table { + var given []string + scanner := bufio.NewScanner(strings.NewReader(entry.input)) + scanner.Split(SplitOnRawMessage) + for scanner.Scan() { + given = append(given, scanner.Text()) + } + + err := scanner.Err() + errorIf(t, err) + assertEqualI(t, i, given, entry.expected) + } +} + +func TestParseMessageParams(t *testing.T) { + type tableT struct { + input string + expected MessageParams + } + table := []tableT { + { + "", + MessageParams { + Middle: []string { }, + Trailing: "", + }, + }, + { + " ", + MessageParams { + Middle: []string { }, + Trailing: "", + }, + }, + { + " :", + MessageParams { + Middle: []string { }, + Trailing: "", + }, + }, + { + " : ", + MessageParams { + Middle: []string { }, + Trailing: " ", + }, + }, + { + ": ", + MessageParams { + Middle: []string { ":" }, + Trailing: "", + }, + }, + { + ": ", + MessageParams { + Middle: []string { ":" }, + Trailing: "", + }, + }, + { + " : ", + MessageParams { + Middle: []string { }, + Trailing: " ", + }, + }, + { + " :", + MessageParams { + Middle: []string { }, + Trailing: "", + }, + }, + { + " :", + MessageParams { + Middle: []string { }, + Trailing: "", + }, + }, + { + "a", + MessageParams { + Middle: []string { "a" }, + Trailing: "", + }, + }, + { + "ab", + MessageParams { + Middle: []string { "ab" }, + Trailing: "", + }, + }, + { + "a b", + MessageParams { + Middle: []string { "a", "b" }, + Trailing: "", + }, + }, + { + "a b c", + MessageParams { + Middle: []string { "a", "b", "c" }, + Trailing: "", + }, + }, + { + "a b:c", + MessageParams { + Middle: []string { "a", "b:c" }, + Trailing: "", + }, + }, + { + "a b:c:", + MessageParams { + Middle: []string { "a", "b:c:" }, + Trailing: "", + }, + }, + { + "a b :c", + MessageParams { + Middle: []string { "a", "b" }, + Trailing: "c", + }, + }, + { + "a b :c:", + MessageParams { + Middle: []string { "a", "b" }, + Trailing: "c:", + }, + }, + { + "a b :c ", + MessageParams { + Middle: []string { "a", "b" }, + Trailing: "c ", + }, + }, + { + "a b : c", + MessageParams { + Middle: []string { "a", "b" }, + Trailing: " c", + }, + }, + { + "a b : c ", + MessageParams { + Middle: []string { "a", "b" }, + Trailing: " c ", + }, + }, + { + "a b : c :", + MessageParams { + Middle: []string { "a", "b" }, + Trailing: " c :", + }, + }, + { + "a b : c : ", + MessageParams { + Middle: []string { "a", "b" }, + Trailing: " c : ", + }, + }, + { + "a b : c :d", + MessageParams { + Middle: []string { "a", "b" }, + Trailing: " c :d", + }, + }, + { + "a b : c :d ", + MessageParams { + Middle: []string { "a", "b" }, + Trailing: " c :d ", + }, + }, + { + "a b : c : d ", + MessageParams { + Middle: []string { "a", "b" }, + Trailing: " c : d ", + }, + }, + } + + for i, entry := range table { + given := ParseMessageParams(entry.input) + assertEqualI(t, i, given, entry.expected) + } +} + +func TestParseMessage(t *testing.T) { + type tableTOK struct { + input string + expected Message + } + tableOK := []tableTOK {{ + "NICK joebloe ", + Message { + Prefix: "", + Command: "NICK", + Params: MessageParams { + Middle: []string { "joebloe" }, + Trailing: "", + }, + Raw: "NICK joebloe ", + }, + }, { + "USER joebloe 0.0.0.0 joe :Joe Bloe", + Message { + Prefix: "", + Command: "USER", + Params: MessageParams { + Middle: []string { + "joebloe", "0.0.0.0", "joe", + }, + Trailing: "Joe Bloe", + }, + Raw: "USER joebloe 0.0.0.0 joe :Joe Bloe", + }, + }, { + ":pre USER joebloe 0.0.0.0 joe :Joe Bloe", + Message { + Prefix: "pre", + Command: "USER", + Params: MessageParams { + Middle: []string { + "joebloe", "0.0.0.0", "joe", + }, + Trailing: "Joe Bloe", + }, + Raw: ":pre USER joebloe 0.0.0.0 joe :Joe Bloe", + }, + }, { + ":pre USER joebloe 0.0.0.0 joe : Joe Bloe ", + Message { + Prefix: "pre", + Command: "USER", + Params: MessageParams { + Middle: []string { + "joebloe", "0.0.0.0", "joe", + }, + Trailing: " Joe Bloe ", + }, + Raw: ":pre USER joebloe 0.0.0.0 joe : Joe Bloe ", + }, + }, { + ":pre USER joebloe: 0:0:0:1 joe::a: : Joe Bloe ", + Message { + Prefix: "pre", + Command: "USER", + Params: MessageParams { + Middle: []string { + "joebloe:", "0:0:0:1", "joe::a:", + }, + Trailing: " Joe Bloe ", + }, + Raw: ":pre USER joebloe: 0:0:0:1 joe::a: : Joe Bloe ", + }, + }, { + ":pre USER :Joe Bloe", + Message { + Prefix: "pre", + Command: "USER", + Params: MessageParams { + Middle: []string { }, + Trailing: "Joe Bloe", + }, + Raw: ":pre USER :Joe Bloe", + }, + }, { + ":pre USER : Joe Bloe", + Message { + Prefix: "pre", + Command: "USER", + Params: MessageParams { + Middle: []string { }, + Trailing: " Joe Bloe", + }, + Raw: ":pre USER : Joe Bloe", + }, + }, { + ":pre USER : Joe Bloe", + Message { + Prefix: "pre", + Command: "USER", + Params: MessageParams { + Middle: []string { }, + Trailing: " Joe Bloe", + }, + Raw: ":pre USER : Joe Bloe", + }, + }, { + ":pre USER : ", + Message { + Prefix: "pre", + Command: "USER", + Params: MessageParams { + Middle: []string { }, + Trailing: " ", + }, + Raw: ":pre USER : ", + }, + }, { + ":pre USER :", + Message { + Prefix: "pre", + Command: "USER", + Params: MessageParams { + Middle: []string { }, + Trailing: "", + }, + Raw: ":pre USER :", + }, + }} + + for i, entry := range tableOK { + given, err := ParseMessage(entry.input) + errorIf(t, err) + assertEqualI(t, i, given, entry.expected) + } + + + type tableErrorT struct { + input string + expected error + } + parseErr := errors.New("Can't parse message") + tableError := []tableErrorT { + { + ":pre", + parseErr, + }, + { + ": pre", + parseErr, + }, + { + ":pre N1CK", + parseErr, + }, + } + + for i, entry := range tableError { + _, given := ParseMessage(entry.input) + assertEqualI(t, i, given, entry.expected) + } +} + +func TestInitMigrations(t *testing.T) { + const query = `SELECT filename FROM migrations;` + + db, err := sql.Open("sqlite3", ":memory:") + g.FatalIf(err) + + _, err = db.Query(query) + assertEqual(t, err.Error(), "no such table: migrations") + + for i := 0; i < 5; i++ { + InitMigrations(db) + rows, err := db.Query(query) + g.FatalIf(err) + assertEqual(t, rows.Next(), false) + g.FatalIf(rows.Err()) + } +} + +func TestPendingMigrations(t *testing.T) { + db, err := sql.Open("sqlite3", ":memory:") + g.FatalIf(err) + + InitMigrations(db) + pending1 := PendingMigrations(db) + pending2 := PendingMigrations(db) + + assertEqual(t, pending1, pending2) +} + +func TestRunMigrations(t *testing.T) { + db, err := sql.Open("sqlite3", ":memory:") + g.FatalIf(err) + + for i := 0; i < 5; i++ { + RunMigrations(db) + } +} + + + +func MainTest() { + tests := []testing.InternalTest { + { "TestSplitOnCRLF", TestSplitOnCRLF }, + { "TestSplitOnRawMessage", TestSplitOnRawMessage }, + { "TestParseMessageParams", TestParseMessageParams }, + { "TestParseMessage", TestParseMessage }, + { "TestInitMigrations", TestInitMigrations }, + { "TestPendingMigrations", TestPendingMigrations }, + { "TestRunMigrations", TestRunMigrations }, + } + + benchmarks := []testing.InternalBenchmark {} + fuzzTargets := []testing.InternalFuzzTarget {} + examples := []testing.InternalExample {} + m := testing.MainStart( + testdeps.TestDeps {}, + tests, + benchmarks, + fuzzTargets, + examples, + ) + os.Exit(m.Run()) +} -- cgit v1.2.3