forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

Compare changes

Choose any two refs to compare.

Changed files
+831 -143
appview
notify
db
pages
templates
knotserver
nix
sets
spindle
engines
nixery
types
+67 -57
appview/notify/db/db.go
···
import (
"context"
"log"
-
"maps"
"slices"
"github.com/bluesky-social/indigo/atproto/syntax"
···
"tangled.org/core/appview/notify"
"tangled.org/core/idresolver"
"tangled.org/core/orm"
+
"tangled.org/core/sets"
)
const (
-
maxMentions = 5
+
maxMentions = 8
)
type databaseNotifier struct {
···
}
actorDid := syntax.DID(star.Did)
-
recipients := []syntax.DID{syntax.DID(repo.Did)}
+
recipients := sets.Singleton(syntax.DID(repo.Did))
eventType := models.NotificationTypeRepoStarred
entityType := "repo"
entityId := star.RepoAt.String()
···
}
func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {
-
-
// build the recipients list
-
// - owner of the repo
-
// - collaborators in the repo
-
var recipients []syntax.DID
-
recipients = append(recipients, syntax.DID(issue.Repo.Did))
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt()))
if err != nil {
log.Printf("failed to fetch collaborators: %v", err)
return
}
+
+
// build the recipients list
+
// - owner of the repo
+
// - collaborators in the repo
+
// - remove users already mentioned
+
recipients := sets.Singleton(syntax.DID(issue.Repo.Did))
for _, c := range collaborators {
-
recipients = append(recipients, c.SubjectDid)
+
recipients.Insert(c.SubjectDid)
+
}
+
for _, m := range mentions {
+
recipients.Remove(m)
}
actorDid := syntax.DID(issue.Did)
···
)
n.notifyEvent(
actorDid,
-
mentions,
+
sets.Collect(slices.Values(mentions)),
models.NotificationTypeUserMentioned,
entityType,
entityId,
···
}
issue := issues[0]
-
var recipients []syntax.DID
-
recipients = append(recipients, syntax.DID(issue.Repo.Did))
+
// built the recipients list:
+
// - the owner of the repo
+
// - | if the comment is a reply -> everybody on that thread
+
// | if the comment is a top level -> just the issue owner
+
// - remove mentioned users from the recipients list
+
recipients := sets.Singleton(syntax.DID(issue.Repo.Did))
if comment.IsReply() {
// if this comment is a reply, then notify everybody in that thread
parentAtUri := *comment.ReplyTo
-
allThreads := issue.CommentList()
// find the parent thread, and add all DIDs from here to the recipient list
-
for _, t := range allThreads {
+
for _, t := range issue.CommentList() {
if t.Self.AtUri().String() == parentAtUri {
-
recipients = append(recipients, t.Participants()...)
+
for _, p := range t.Participants() {
+
recipients.Insert(p)
+
}
}
}
} else {
// not a reply, notify just the issue author
-
recipients = append(recipients, syntax.DID(issue.Did))
+
recipients.Insert(syntax.DID(issue.Did))
+
}
+
+
for _, m := range mentions {
+
recipients.Remove(m)
}
actorDid := syntax.DID(comment.Did)
···
)
n.notifyEvent(
actorDid,
-
mentions,
+
sets.Collect(slices.Values(mentions)),
models.NotificationTypeUserMentioned,
entityType,
entityId,
···
func (n *databaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) {
actorDid := syntax.DID(follow.UserDid)
-
recipients := []syntax.DID{syntax.DID(follow.SubjectDid)}
+
recipients := sets.Singleton(syntax.DID(follow.SubjectDid))
eventType := models.NotificationTypeFollowed
entityType := "follow"
entityId := follow.UserDid
···
log.Printf("NewPull: failed to get repos: %v", err)
return
}
-
-
// build the recipients list
-
// - owner of the repo
-
// - collaborators in the repo
-
var recipients []syntax.DID
-
recipients = append(recipients, syntax.DID(repo.Did))
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt()))
if err != nil {
log.Printf("failed to fetch collaborators: %v", err)
return
}
+
+
// build the recipients list
+
// - owner of the repo
+
// - collaborators in the repo
+
recipients := sets.Singleton(syntax.DID(repo.Did))
for _, c := range collaborators {
-
recipients = append(recipients, c.SubjectDid)
+
recipients.Insert(c.SubjectDid)
}
actorDid := syntax.DID(pull.OwnerDid)
···
// build up the recipients list:
// - repo owner
// - all pull participants
-
var recipients []syntax.DID
-
recipients = append(recipients, syntax.DID(repo.Did))
+
// - remove those already mentioned
+
recipients := sets.Singleton(syntax.DID(repo.Did))
for _, p := range pull.Participants() {
-
recipients = append(recipients, syntax.DID(p))
+
recipients.Insert(syntax.DID(p))
+
}
+
for _, m := range mentions {
+
recipients.Remove(m)
}
actorDid := syntax.DID(comment.OwnerDid)
···
)
n.notifyEvent(
actorDid,
-
mentions,
+
sets.Collect(slices.Values(mentions)),
models.NotificationTypeUserMentioned,
entityType,
entityId,
···
}
func (n *databaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {
-
// build up the recipients list:
-
// - repo owner
-
// - repo collaborators
-
// - all issue participants
-
var recipients []syntax.DID
-
recipients = append(recipients, syntax.DID(issue.Repo.Did))
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt()))
if err != nil {
log.Printf("failed to fetch collaborators: %v", err)
return
}
+
+
// build up the recipients list:
+
// - repo owner
+
// - repo collaborators
+
// - all issue participants
+
recipients := sets.Singleton(syntax.DID(issue.Repo.Did))
for _, c := range collaborators {
-
recipients = append(recipients, c.SubjectDid)
+
recipients.Insert(c.SubjectDid)
}
for _, p := range issue.Participants() {
-
recipients = append(recipients, syntax.DID(p))
+
recipients.Insert(syntax.DID(p))
}
entityType := "pull"
···
return
}
-
// build up the recipients list:
-
// - repo owner
-
// - all pull participants
-
var recipients []syntax.DID
-
recipients = append(recipients, syntax.DID(repo.Did))
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt()))
if err != nil {
log.Printf("failed to fetch collaborators: %v", err)
return
}
+
+
// build up the recipients list:
+
// - repo owner
+
// - all pull participants
+
recipients := sets.Singleton(syntax.DID(repo.Did))
for _, c := range collaborators {
-
recipients = append(recipients, c.SubjectDid)
+
recipients.Insert(c.SubjectDid)
}
for _, p := range pull.Participants() {
-
recipients = append(recipients, syntax.DID(p))
+
recipients.Insert(syntax.DID(p))
}
entityType := "pull"
···
func (n *databaseNotifier) notifyEvent(
actorDid syntax.DID,
-
recipients []syntax.DID,
+
recipients sets.Set[syntax.DID],
eventType models.NotificationType,
entityType string,
entityId string,
···
issueId *int64,
pullId *int64,
) {
-
if eventType == models.NotificationTypeUserMentioned && len(recipients) > maxMentions {
-
recipients = recipients[:maxMentions]
+
// if the user is attempting to mention >maxMentions users, this is probably spam, do not mention anybody
+
if eventType == models.NotificationTypeUserMentioned && recipients.Len() > maxMentions {
+
return
}
-
recipientSet := make(map[syntax.DID]struct{})
-
for _, did := range recipients {
-
// everybody except actor themselves
-
if did != actorDid {
-
recipientSet[did] = struct{}{}
-
}
-
}
+
+
recipients.Remove(actorDid)
prefMap, err := db.GetNotificationPreferences(
n.db,
-
orm.FilterIn("user_did", slices.Collect(maps.Keys(recipientSet))),
+
orm.FilterIn("user_did", slices.Collect(recipients.All())),
)
if err != nil {
// failed to get prefs for users
···
defer tx.Rollback()
// filter based on preferences
-
for recipientDid := range recipientSet {
+
for recipientDid := range recipients.All() {
prefs, ok := prefMap[recipientDid]
if !ok {
prefs = models.DefaultNotificationPreferences(recipientDid)
+9 -6
appview/pages/templates/user/signup.html
···
page to complete your registration.
</span>
<div class="w-full mt-4 text-center">
-
<div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}"></div>
+
<div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}" data-size="flexible"></div>
</div>
<button class="btn text-base w-full my-2 mt-6" type="submit" id="signup-button" tabindex="7" >
<span>join now</span>
</button>
+
<p class="text-sm text-gray-500">
+
Already have an AT Protocol account? <a href="/login" class="underline">Login to Tangled</a>.
+
</p>
+
+
<p id="signup-msg" class="error w-full"></p>
+
<p class="text-sm text-gray-500 pt-4">
+
By signing up, you agree to our <a href="/terms" class="underline">Terms of Service</a> and <a href="/privacy" class="underline">Privacy Policy</a>.
+
</p>
</form>
-
<p class="text-sm text-gray-500">
-
Already have an AT Protocol account? <a href="/login" class="underline">Login to Tangled</a>.
-
</p>
-
-
<p id="signup-msg" class="error w-full"></p>
</main>
</body>
</html>
+3 -3
flake.lock
···
},
"nixpkgs": {
"locked": {
-
"lastModified": 1751984180,
-
"narHash": "sha256-LwWRsENAZJKUdD3SpLluwDmdXY9F45ZEgCb0X+xgOL0=",
+
"lastModified": 1765186076,
+
"narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=",
"owner": "nixos",
"repo": "nixpkgs",
-
"rev": "9807714d6944a957c2e036f84b0ff8caf9930bc0",
+
"rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8",
"type": "github"
},
"original": {
-2
flake.nix
···
}).buildGoApplication;
modules = ./nix/gomod2nix.toml;
sqlite-lib = self.callPackage ./nix/pkgs/sqlite-lib.nix {
-
inherit (pkgs) gcc;
inherit sqlite-lib-src;
};
lexgen = self.callPackage ./nix/pkgs/lexgen.nix {inherit indigo;};
···
nativeBuildInputs = [
pkgs.go
pkgs.air
-
pkgs.tilt
pkgs.gopls
pkgs.httpie
pkgs.litecli
+1 -1
go.mod
···
module tangled.org/core
-
go 1.24.4
+
go 1.25.0
require (
github.com/Blank-Xu/sql-adapter v1.1.1
+81
knotserver/db/db.go
···
+
package db
+
+
import (
+
"context"
+
"database/sql"
+
"log/slog"
+
"strings"
+
+
_ "github.com/mattn/go-sqlite3"
+
"tangled.org/core/log"
+
)
+
+
type DB struct {
+
db *sql.DB
+
logger *slog.Logger
+
}
+
+
func Setup(ctx context.Context, dbPath string) (*DB, error) {
+
// https://github.com/mattn/go-sqlite3#connection-string
+
opts := []string{
+
"_foreign_keys=1",
+
"_journal_mode=WAL",
+
"_synchronous=NORMAL",
+
"_auto_vacuum=incremental",
+
}
+
+
logger := log.FromContext(ctx)
+
logger = log.SubLogger(logger, "db")
+
+
db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&"))
+
if err != nil {
+
return nil, err
+
}
+
+
conn, err := db.Conn(ctx)
+
if err != nil {
+
return nil, err
+
}
+
defer conn.Close()
+
+
_, err = conn.ExecContext(ctx, `
+
create table if not exists known_dids (
+
did text primary key
+
);
+
+
create table if not exists public_keys (
+
id integer primary key autoincrement,
+
did text not null,
+
key text not null,
+
created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
+
unique(did, key),
+
foreign key (did) references known_dids(did) on delete cascade
+
);
+
+
create table if not exists _jetstream (
+
id integer primary key autoincrement,
+
last_time_us integer not null
+
);
+
+
create table if not exists events (
+
rkey text not null,
+
nsid text not null,
+
event text not null, -- json
+
created integer not null default (strftime('%s', 'now')),
+
primary key (rkey, nsid)
+
);
+
+
create table if not exists migrations (
+
id integer primary key autoincrement,
+
name text unique
+
);
+
`)
+
if err != nil {
+
return nil, err
+
}
+
+
return &DB{
+
db: db,
+
logger: logger,
+
}, nil
+
}
-64
knotserver/db/init.go
···
-
package db
-
-
import (
-
"database/sql"
-
"strings"
-
-
_ "github.com/mattn/go-sqlite3"
-
)
-
-
type DB struct {
-
db *sql.DB
-
}
-
-
func Setup(dbPath string) (*DB, error) {
-
// https://github.com/mattn/go-sqlite3#connection-string
-
opts := []string{
-
"_foreign_keys=1",
-
"_journal_mode=WAL",
-
"_synchronous=NORMAL",
-
"_auto_vacuum=incremental",
-
}
-
-
db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&"))
-
if err != nil {
-
return nil, err
-
}
-
-
// NOTE: If any other migration is added here, you MUST
-
// copy the pattern in appview: use a single sql.Conn
-
// for every migration.
-
-
_, err = db.Exec(`
-
create table if not exists known_dids (
-
did text primary key
-
);
-
-
create table if not exists public_keys (
-
id integer primary key autoincrement,
-
did text not null,
-
key text not null,
-
created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
-
unique(did, key),
-
foreign key (did) references known_dids(did) on delete cascade
-
);
-
-
create table if not exists _jetstream (
-
id integer primary key autoincrement,
-
last_time_us integer not null
-
);
-
-
create table if not exists events (
-
rkey text not null,
-
nsid text not null,
-
event text not null, -- json
-
created integer not null default (strftime('%s', 'now')),
-
primary key (rkey, nsid)
-
);
-
`)
-
if err != nil {
-
return nil, err
-
}
-
-
return &DB{db: db}, nil
-
}
+1 -1
knotserver/server.go
···
logger.Info("running in dev mode, signature verification is disabled")
}
-
db, err := db.Setup(c.Server.DBPath)
+
db, err := db.Setup(ctx, c.Server.DBPath)
if err != nil {
return fmt.Errorf("failed to load db: %w", err)
}
+7 -5
nix/pkgs/sqlite-lib.nix
···
{
-
gcc,
stdenv,
sqlite-lib-src,
}:
stdenv.mkDerivation {
name = "sqlite-lib";
src = sqlite-lib-src;
-
nativeBuildInputs = [gcc];
+
buildPhase = ''
-
gcc -c sqlite3.c
-
ar rcs libsqlite3.a sqlite3.o
-
ranlib libsqlite3.a
+
$CC -c sqlite3.c
+
$AR rcs libsqlite3.a sqlite3.o
+
$RANLIB libsqlite3.a
+
'';
+
+
installPhase = ''
mkdir -p $out/include $out/lib
cp *.h $out/include
cp libsqlite3.a $out/lib
+31
sets/gen.go
···
+
package sets
+
+
import (
+
"math/rand"
+
"reflect"
+
"testing/quick"
+
)
+
+
func (_ Set[T]) Generate(rand *rand.Rand, size int) reflect.Value {
+
s := New[T]()
+
+
var zero T
+
itemType := reflect.TypeOf(zero)
+
+
for {
+
if s.Len() >= size {
+
break
+
}
+
+
item, ok := quick.Value(itemType, rand)
+
if !ok {
+
continue
+
}
+
+
if val, ok := item.Interface().(T); ok {
+
s.Insert(val)
+
}
+
}
+
+
return reflect.ValueOf(s)
+
}
+35
sets/readme.txt
···
+
sets
+
----
+
set datastructure for go with generics and iterators. the
+
api is supposed to mimic rust's std::collections::HashSet api.
+
+
s1 := sets.Collect(slices.Values([]int{1, 2, 3, 4}))
+
s2 := sets.Collect(slices.Values([]int{1, 2, 3, 4, 5, 6}))
+
+
union := sets.Collect(s1.Union(s2))
+
intersect := sets.Collect(s1.Intersection(s2))
+
diff := sets.Collect(s1.Difference(s2))
+
symdiff := sets.Collect(s1.SymmetricDifference(s2))
+
+
s1.Len() // 4
+
s1.Contains(1) // true
+
s1.IsEmpty() // false
+
s1.IsSubset(s2) // true
+
s1.IsSuperset(s2) // false
+
s1.IsDisjoint(s2) // false
+
+
if exists := s1.Insert(1); exists {
+
// already existed in set
+
}
+
+
if existed := s1.Remove(1); existed {
+
// existed in set, now removed
+
}
+
+
+
testing
+
-------
+
includes property-based tests using the wonderful
+
testing/quick module!
+
+
go test -v
+174
sets/set.go
···
+
package sets
+
+
import (
+
"iter"
+
"maps"
+
)
+
+
type Set[T comparable] struct {
+
data map[T]struct{}
+
}
+
+
func New[T comparable]() Set[T] {
+
return Set[T]{
+
data: make(map[T]struct{}),
+
}
+
}
+
+
func (s *Set[T]) Insert(item T) bool {
+
_, exists := s.data[item]
+
s.data[item] = struct{}{}
+
return !exists
+
}
+
+
func Singleton[T comparable](item T) Set[T] {
+
n := New[T]()
+
_ = n.Insert(item)
+
return n
+
}
+
+
func (s *Set[T]) Remove(item T) bool {
+
_, exists := s.data[item]
+
if exists {
+
delete(s.data, item)
+
}
+
return exists
+
}
+
+
func (s Set[T]) Contains(item T) bool {
+
_, exists := s.data[item]
+
return exists
+
}
+
+
func (s Set[T]) Len() int {
+
return len(s.data)
+
}
+
+
func (s Set[T]) IsEmpty() bool {
+
return len(s.data) == 0
+
}
+
+
func (s *Set[T]) Clear() {
+
s.data = make(map[T]struct{})
+
}
+
+
func (s Set[T]) All() iter.Seq[T] {
+
return func(yield func(T) bool) {
+
for item := range s.data {
+
if !yield(item) {
+
return
+
}
+
}
+
}
+
}
+
+
func (s Set[T]) Clone() Set[T] {
+
return Set[T]{
+
data: maps.Clone(s.data),
+
}
+
}
+
+
func (s Set[T]) Union(other Set[T]) iter.Seq[T] {
+
if s.Len() >= other.Len() {
+
return chain(s.All(), other.Difference(s))
+
} else {
+
return chain(other.All(), s.Difference(other))
+
}
+
}
+
+
func chain[T any](seqs ...iter.Seq[T]) iter.Seq[T] {
+
return func(yield func(T) bool) {
+
for _, seq := range seqs {
+
for item := range seq {
+
if !yield(item) {
+
return
+
}
+
}
+
}
+
}
+
}
+
+
func (s Set[T]) Intersection(other Set[T]) iter.Seq[T] {
+
return func(yield func(T) bool) {
+
for item := range s.data {
+
if other.Contains(item) {
+
if !yield(item) {
+
return
+
}
+
}
+
}
+
}
+
}
+
+
func (s Set[T]) Difference(other Set[T]) iter.Seq[T] {
+
return func(yield func(T) bool) {
+
for item := range s.data {
+
if !other.Contains(item) {
+
if !yield(item) {
+
return
+
}
+
}
+
}
+
}
+
}
+
+
func (s Set[T]) SymmetricDifference(other Set[T]) iter.Seq[T] {
+
return func(yield func(T) bool) {
+
for item := range s.data {
+
if !other.Contains(item) {
+
if !yield(item) {
+
return
+
}
+
}
+
}
+
for item := range other.data {
+
if !s.Contains(item) {
+
if !yield(item) {
+
return
+
}
+
}
+
}
+
}
+
}
+
+
func (s Set[T]) IsSubset(other Set[T]) bool {
+
for item := range s.data {
+
if !other.Contains(item) {
+
return false
+
}
+
}
+
return true
+
}
+
+
func (s Set[T]) IsSuperset(other Set[T]) bool {
+
return other.IsSubset(s)
+
}
+
+
func (s Set[T]) IsDisjoint(other Set[T]) bool {
+
for item := range s.data {
+
if other.Contains(item) {
+
return false
+
}
+
}
+
return true
+
}
+
+
func (s Set[T]) Equal(other Set[T]) bool {
+
if s.Len() != other.Len() {
+
return false
+
}
+
for item := range s.data {
+
if !other.Contains(item) {
+
return false
+
}
+
}
+
return true
+
}
+
+
func Collect[T comparable](seq iter.Seq[T]) Set[T] {
+
result := New[T]()
+
for item := range seq {
+
result.Insert(item)
+
}
+
return result
+
}
+411
sets/set_test.go
···
+
package sets
+
+
import (
+
"slices"
+
"testing"
+
"testing/quick"
+
)
+
+
func TestNew(t *testing.T) {
+
s := New[int]()
+
if s.Len() != 0 {
+
t.Errorf("New set should be empty, got length %d", s.Len())
+
}
+
if !s.IsEmpty() {
+
t.Error("New set should be empty")
+
}
+
}
+
+
func TestFromSlice(t *testing.T) {
+
s := Collect(slices.Values([]int{1, 2, 3, 2, 1}))
+
if s.Len() != 3 {
+
t.Errorf("Expected length 3, got %d", s.Len())
+
}
+
if !s.Contains(1) || !s.Contains(2) || !s.Contains(3) {
+
t.Error("Set should contain all unique elements from slice")
+
}
+
}
+
+
func TestInsert(t *testing.T) {
+
s := New[string]()
+
+
if !s.Insert("hello") {
+
t.Error("First insert should return true")
+
}
+
if s.Insert("hello") {
+
t.Error("Duplicate insert should return false")
+
}
+
if s.Len() != 1 {
+
t.Errorf("Expected length 1, got %d", s.Len())
+
}
+
}
+
+
func TestRemove(t *testing.T) {
+
s := Collect(slices.Values([]int{1, 2, 3}))
+
+
if !s.Remove(2) {
+
t.Error("Remove existing element should return true")
+
}
+
if s.Remove(2) {
+
t.Error("Remove non-existing element should return false")
+
}
+
if s.Contains(2) {
+
t.Error("Element should be removed")
+
}
+
if s.Len() != 2 {
+
t.Errorf("Expected length 2, got %d", s.Len())
+
}
+
}
+
+
func TestContains(t *testing.T) {
+
s := Collect(slices.Values([]int{1, 2, 3}))
+
+
if !s.Contains(1) {
+
t.Error("Should contain 1")
+
}
+
if s.Contains(4) {
+
t.Error("Should not contain 4")
+
}
+
}
+
+
func TestClear(t *testing.T) {
+
s := Collect(slices.Values([]int{1, 2, 3}))
+
s.Clear()
+
+
if !s.IsEmpty() {
+
t.Error("Set should be empty after clear")
+
}
+
if s.Len() != 0 {
+
t.Errorf("Expected length 0, got %d", s.Len())
+
}
+
}
+
+
func TestIterator(t *testing.T) {
+
s := Collect(slices.Values([]int{1, 2, 3}))
+
var items []int
+
+
for item := range s.All() {
+
items = append(items, item)
+
}
+
+
slices.Sort(items)
+
expected := []int{1, 2, 3}
+
if !slices.Equal(items, expected) {
+
t.Errorf("Expected %v, got %v", expected, items)
+
}
+
}
+
+
func TestClone(t *testing.T) {
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
+
s2 := s1.Clone()
+
+
if !s1.Equal(s2) {
+
t.Error("Cloned set should be equal to original")
+
}
+
+
s2.Insert(4)
+
if s1.Contains(4) {
+
t.Error("Modifying clone should not affect original")
+
}
+
}
+
+
func TestUnion(t *testing.T) {
+
s1 := Collect(slices.Values([]int{1, 2}))
+
s2 := Collect(slices.Values([]int{2, 3}))
+
+
result := Collect(s1.Union(s2))
+
expected := Collect(slices.Values([]int{1, 2, 3}))
+
+
if !result.Equal(expected) {
+
t.Errorf("Expected %v, got %v", expected, result)
+
}
+
}
+
+
func TestIntersection(t *testing.T) {
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
+
s2 := Collect(slices.Values([]int{2, 3, 4}))
+
+
expected := Collect(slices.Values([]int{2, 3}))
+
result := Collect(s1.Intersection(s2))
+
+
if !result.Equal(expected) {
+
t.Errorf("Expected %v, got %v", expected, result)
+
}
+
}
+
+
func TestDifference(t *testing.T) {
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
+
s2 := Collect(slices.Values([]int{2, 3, 4}))
+
+
expected := Collect(slices.Values([]int{1}))
+
result := Collect(s1.Difference(s2))
+
+
if !result.Equal(expected) {
+
t.Errorf("Expected %v, got %v", expected, result)
+
}
+
}
+
+
func TestSymmetricDifference(t *testing.T) {
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
+
s2 := Collect(slices.Values([]int{2, 3, 4}))
+
+
expected := Collect(slices.Values([]int{1, 4}))
+
result := Collect(s1.SymmetricDifference(s2))
+
+
if !result.Equal(expected) {
+
t.Errorf("Expected %v, got %v", expected, result)
+
}
+
}
+
+
func TestSymmetricDifferenceCommutativeProperty(t *testing.T) {
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
+
s2 := Collect(slices.Values([]int{2, 3, 4}))
+
+
result1 := Collect(s1.SymmetricDifference(s2))
+
result2 := Collect(s2.SymmetricDifference(s1))
+
+
if !result1.Equal(result2) {
+
t.Errorf("Expected %v, got %v", result1, result2)
+
}
+
}
+
+
func TestIsSubset(t *testing.T) {
+
s1 := Collect(slices.Values([]int{1, 2}))
+
s2 := Collect(slices.Values([]int{1, 2, 3}))
+
+
if !s1.IsSubset(s2) {
+
t.Error("s1 should be subset of s2")
+
}
+
if s2.IsSubset(s1) {
+
t.Error("s2 should not be subset of s1")
+
}
+
}
+
+
func TestIsSuperset(t *testing.T) {
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
+
s2 := Collect(slices.Values([]int{1, 2}))
+
+
if !s1.IsSuperset(s2) {
+
t.Error("s1 should be superset of s2")
+
}
+
if s2.IsSuperset(s1) {
+
t.Error("s2 should not be superset of s1")
+
}
+
}
+
+
func TestIsDisjoint(t *testing.T) {
+
s1 := Collect(slices.Values([]int{1, 2}))
+
s2 := Collect(slices.Values([]int{3, 4}))
+
s3 := Collect(slices.Values([]int{2, 3}))
+
+
if !s1.IsDisjoint(s2) {
+
t.Error("s1 and s2 should be disjoint")
+
}
+
if s1.IsDisjoint(s3) {
+
t.Error("s1 and s3 should not be disjoint")
+
}
+
}
+
+
func TestEqual(t *testing.T) {
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
+
s2 := Collect(slices.Values([]int{3, 2, 1}))
+
s3 := Collect(slices.Values([]int{1, 2}))
+
+
if !s1.Equal(s2) {
+
t.Error("s1 and s2 should be equal")
+
}
+
if s1.Equal(s3) {
+
t.Error("s1 and s3 should not be equal")
+
}
+
}
+
+
func TestCollect(t *testing.T) {
+
s1 := Collect(slices.Values([]int{1, 2}))
+
s2 := Collect(slices.Values([]int{2, 3}))
+
+
unionSet := Collect(s1.Union(s2))
+
if unionSet.Len() != 3 {
+
t.Errorf("Expected union set length 3, got %d", unionSet.Len())
+
}
+
if !unionSet.Contains(1) || !unionSet.Contains(2) || !unionSet.Contains(3) {
+
t.Error("Union set should contain 1, 2, and 3")
+
}
+
+
diffSet := Collect(s1.Difference(s2))
+
if diffSet.Len() != 1 {
+
t.Errorf("Expected difference set length 1, got %d", diffSet.Len())
+
}
+
if !diffSet.Contains(1) {
+
t.Error("Difference set should contain 1")
+
}
+
}
+
+
func TestPropertySingleonLen(t *testing.T) {
+
f := func(item int) bool {
+
single := Singleton(item)
+
return single.Len() == 1
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertyInsertIdempotent(t *testing.T) {
+
f := func(s Set[int], item int) bool {
+
clone := s.Clone()
+
+
clone.Insert(item)
+
firstLen := clone.Len()
+
+
clone.Insert(item)
+
secondLen := clone.Len()
+
+
return firstLen == secondLen
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertyUnionCommutative(t *testing.T) {
+
f := func(s1 Set[int], s2 Set[int]) bool {
+
union1 := Collect(s1.Union(s2))
+
union2 := Collect(s2.Union(s1))
+
return union1.Equal(union2)
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertyIntersectionCommutative(t *testing.T) {
+
f := func(s1 Set[int], s2 Set[int]) bool {
+
inter1 := Collect(s1.Intersection(s2))
+
inter2 := Collect(s2.Intersection(s1))
+
return inter1.Equal(inter2)
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertyCloneEquals(t *testing.T) {
+
f := func(s Set[int]) bool {
+
clone := s.Clone()
+
return s.Equal(clone)
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertyIntersectionIsSubset(t *testing.T) {
+
f := func(s1 Set[int], s2 Set[int]) bool {
+
inter := Collect(s1.Intersection(s2))
+
return inter.IsSubset(s1) && inter.IsSubset(s2)
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertyUnionIsSuperset(t *testing.T) {
+
f := func(s1 Set[int], s2 Set[int]) bool {
+
union := Collect(s1.Union(s2))
+
return union.IsSuperset(s1) && union.IsSuperset(s2)
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertyDifferenceDisjoint(t *testing.T) {
+
f := func(s1 Set[int], s2 Set[int]) bool {
+
diff := Collect(s1.Difference(s2))
+
return diff.IsDisjoint(s2)
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertySymmetricDifferenceCommutative(t *testing.T) {
+
f := func(s1 Set[int], s2 Set[int]) bool {
+
symDiff1 := Collect(s1.SymmetricDifference(s2))
+
symDiff2 := Collect(s2.SymmetricDifference(s1))
+
return symDiff1.Equal(symDiff2)
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertyRemoveWorks(t *testing.T) {
+
f := func(s Set[int], item int) bool {
+
clone := s.Clone()
+
clone.Insert(item)
+
clone.Remove(item)
+
return !clone.Contains(item)
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertyClearEmpty(t *testing.T) {
+
f := func(s Set[int]) bool {
+
s.Clear()
+
return s.IsEmpty() && s.Len() == 0
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertyIsSubsetReflexive(t *testing.T) {
+
f := func(s Set[int]) bool {
+
return s.IsSubset(s)
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+
+
func TestPropertyDeMorganUnion(t *testing.T) {
+
f := func(s1 Set[int], s2 Set[int], universe Set[int]) bool {
+
// create a universe that contains both sets
+
u := universe.Clone()
+
for item := range s1.All() {
+
u.Insert(item)
+
}
+
for item := range s2.All() {
+
u.Insert(item)
+
}
+
+
// (A u B)' = A' n B'
+
union := Collect(s1.Union(s2))
+
complementUnion := Collect(u.Difference(union))
+
+
complementS1 := Collect(u.Difference(s1))
+
complementS2 := Collect(u.Difference(s2))
+
intersectionComplements := Collect(complementS1.Intersection(complementS2))
+
+
return complementUnion.Equal(intersectionComplements)
+
}
+
+
if err := quick.Check(f, nil); err != nil {
+
t.Error(err)
+
}
+
}
+5 -3
spindle/engines/nixery/engine.go
···
workflowEnvs.AddEnv(s.Key, s.Value)
}
-
step := w.Steps[idx].(Step)
+
step := w.Steps[idx]
select {
case <-ctx.Done():
···
}
envs := append(EnvVars(nil), workflowEnvs...)
-
for k, v := range step.environment {
-
envs.AddEnv(k, v)
+
if nixStep, ok := step.(Step); ok {
+
for k, v := range nixStep.environment {
+
envs.AddEnv(k, v)
+
}
}
envs.AddEnv("HOME", homeDir)
+6 -1
types/commit.go
···
func (commit Commit) CoAuthors() []object.Signature {
var coAuthors []object.Signature
-
+
seen := make(map[string]bool)
matches := coAuthorRegex.FindAllStringSubmatch(commit.Message, -1)
for _, match := range matches {
if len(match) >= 3 {
name := strings.TrimSpace(match[1])
email := strings.TrimSpace(match[2])
+
+
if seen[email] {
+
continue
+
}
+
seen[email] = true
coAuthors = append(coAuthors, object.Signature{
Name: name,