mirror of
https://github.com/nkanaev/yarr.git
synced 2026-06-24 17:15:17 +00:00
Compare commits
6 Commits
14b06dcbaf
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6db9a4b556 | ||
|
|
c90c40aba1 | ||
|
|
41faa8c088 | ||
|
|
c447372fe2 | ||
|
|
2f39fcc6f6 | ||
|
|
21c7f9a4a4 |
7
.github/workflows/build-docker.yml
vendored
7
.github/workflows/build-docker.yml
vendored
@@ -3,6 +3,8 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- v*
|
- v*
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -37,6 +39,11 @@ jobs:
|
|||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=schedule
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=tag
|
||||||
|
type=raw,value=bleeding,enable=${{ github.ref_name == 'master' }}
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
|
|||||||
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
@@ -12,11 +12,11 @@ jobs:
|
|||||||
runs-on: macos-13
|
runs-on: macos-13
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '^1.23'
|
go-version-file: 'go.mod'
|
||||||
- name: Build arm64 gui
|
- name: Build arm64 gui
|
||||||
uses: ./.github/actions/prepare
|
uses: ./.github/actions/prepare
|
||||||
with:
|
with:
|
||||||
@@ -47,11 +47,11 @@ jobs:
|
|||||||
runs-on: windows-2022
|
runs-on: windows-2022
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '^1.23'
|
go-version-file: 'go.mod'
|
||||||
- name: Build amd64 gui
|
- name: Build amd64 gui
|
||||||
uses: ./.github/actions/prepare
|
uses: ./.github/actions/prepare
|
||||||
with:
|
with:
|
||||||
@@ -71,11 +71,11 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '^1.23'
|
go-version-file: 'go.mod'
|
||||||
- name: Setup Zig
|
- name: Setup Zig
|
||||||
uses: mlugg/setup-zig@v1
|
uses: mlugg/setup-zig@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
9
.github/workflows/test.yml
vendored
9
.github/workflows/test.yml
vendored
@@ -6,14 +6,17 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
YARR_POSTGRES_TEST_IMAGE: postgres:17-alpine
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '^1.18'
|
go-version-file: 'go.mod'
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: make test
|
run: make test
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func main() {
|
|||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if ver {
|
if ver {
|
||||||
fmt.Printf("v%s (%s)\n", Version, GitHash)
|
fmt.Printf("%s (%s)\n", Version, GitHash)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
# upcoming
|
# upcoming
|
||||||
|
|
||||||
|
- (new) initial PostgreSQL support
|
||||||
|
- (new) i18n: English, Chinese, French, German, Japanese, Portuguese, Russian, Spanish
|
||||||
- (fix) articles not resetting immediately after feed/filter selection (thank to @scratchmex for the report)
|
- (fix) articles not resetting immediately after feed/filter selection (thank to @scratchmex for the report)
|
||||||
- (fix) crash on empty article list with article is selected (thanks to @rksvc)
|
- (fix) crash on empty article list with article is selected (thanks to @rksvc)
|
||||||
- (fix) invalid article title in RSS feeds with media containing titles (thanks to @bwwu-git for the report)
|
- (fix) invalid article title in RSS feeds with media containing titles (thanks to @bwwu-git for the report)
|
||||||
- (fix) missing image enclosures in certain RSS feeds (thanks to @palinek for the report)
|
- (fix) missing image enclosures in certain RSS feeds (thanks to @palinek for the report)
|
||||||
- (fix) parsing namespaced legacy RSS feeds (thanks to @f100024)
|
- (fix) parsing namespaced legacy RSS feeds (thanks to @f100024)
|
||||||
|
- (fix) marking feeds read in Fever API (thanks to @weskoop)
|
||||||
|
- (etc) systray improvements for macOS
|
||||||
|
|
||||||
# v2.6 (2025-11-24)
|
# v2.6 (2025-11-24)
|
||||||
|
|
||||||
|
|||||||
@@ -51,8 +51,9 @@ while [[ $# -gt 0 ]]; do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# Replace dots with commas for version_comma
|
# Strip leading 'v' and replace dots with commas for version_comma
|
||||||
version_comma="${version//./,}"
|
version_num="${version#v}"
|
||||||
|
version_comma="${version_num//./,}"
|
||||||
|
|
||||||
# Use a here document for the template with ENDFILE delimiter
|
# Use a here document for the template with ENDFILE delimiter
|
||||||
cat <<ENDFILE > "$outfile"
|
cat <<ENDFILE > "$outfile"
|
||||||
|
|||||||
4
go.mod
4
go.mod
@@ -1,8 +1,6 @@
|
|||||||
module github.com/nkanaev/yarr
|
module github.com/nkanaev/yarr
|
||||||
|
|
||||||
go 1.23.0
|
go 1.25.0
|
||||||
|
|
||||||
toolchain go1.23.5
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
fyne.io/systray v1.12.0
|
fyne.io/systray v1.12.0
|
||||||
|
|||||||
2
makefile
2
makefile
@@ -1,4 +1,4 @@
|
|||||||
VERSION=2.6
|
VERSION=$(shell git describe --exact-match --tags HEAD 2>/dev/null || echo bleeding)
|
||||||
GITHASH=$(shell git rev-parse --short=8 HEAD)
|
GITHASH=$(shell git rev-parse --short=8 HEAD)
|
||||||
|
|
||||||
GO_TAGS = sqlite_foreign_keys sqlite_json sqlite_fts5
|
GO_TAGS = sqlite_foreign_keys sqlite_json sqlite_fts5
|
||||||
|
|||||||
@@ -291,14 +291,12 @@ func TestListItemsPaginated(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMarkItemsRead(t *testing.T) {
|
func TestMarkAllItemsRead(t *testing.T) {
|
||||||
// NOTE: starred items must not be marked as read
|
|
||||||
var read model.ItemStatus = model.READ
|
var read model.ItemStatus = model.READ
|
||||||
|
dbtest(t, func(t *testing.T, db storage.Storage) {
|
||||||
dbtest(t, func(t *testing.T, db1 storage.Storage) {
|
testItemsSetup(db)
|
||||||
testItemsSetup(db1)
|
db.MarkItemsRead(model.MarkFilter{})
|
||||||
db1.MarkItemsRead(model.MarkFilter{})
|
have := getItemGuids(db.ListItems(model.ItemFilter{Status: &read}, 10, false, false))
|
||||||
have := getItemGuids(db1.ListItems(model.ItemFilter{Status: &read}, 10, false, false))
|
|
||||||
want := []string{
|
want := []string{
|
||||||
"item111", "item112", "item121", "item122",
|
"item111", "item112", "item121", "item122",
|
||||||
"item211", "item011", "item012",
|
"item211", "item011", "item012",
|
||||||
@@ -309,11 +307,14 @@ func TestMarkItemsRead(t *testing.T) {
|
|||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
dbtest(t, func(t *testing.T, db2 storage.Storage) {
|
func TestMarkItemsReadByFolder(t *testing.T) {
|
||||||
scope2 := testItemsSetup(db2)
|
var read model.ItemStatus = model.READ
|
||||||
db2.MarkItemsRead(model.MarkFilter{FolderID: &scope2.folder1.Id})
|
dbtest(t, func(t *testing.T, db storage.Storage) {
|
||||||
have := getItemGuids(db2.ListItems(model.ItemFilter{Status: &read}, 10, false, false))
|
scope := testItemsSetup(db)
|
||||||
|
db.MarkItemsRead(model.MarkFilter{FolderID: &scope.folder1.Id})
|
||||||
|
have := getItemGuids(db.ListItems(model.ItemFilter{Status: &read}, 10, false, false))
|
||||||
want := []string{
|
want := []string{
|
||||||
"item111", "item112", "item121", "item122",
|
"item111", "item112", "item121", "item122",
|
||||||
"item211", "item012",
|
"item211", "item012",
|
||||||
@@ -324,11 +325,14 @@ func TestMarkItemsRead(t *testing.T) {
|
|||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
dbtest(t, func(t *testing.T, db3 storage.Storage) {
|
func TestMarkItemsReadByFeed(t *testing.T) {
|
||||||
scope3 := testItemsSetup(db3)
|
var read model.ItemStatus = model.READ
|
||||||
db3.MarkItemsRead(model.MarkFilter{FeedID: &scope3.feed11.Id})
|
dbtest(t, func(t *testing.T, db storage.Storage) {
|
||||||
have := getItemGuids(db3.ListItems(model.ItemFilter{Status: &read}, 10, false, false))
|
scope := testItemsSetup(db)
|
||||||
|
db.MarkItemsRead(model.MarkFilter{FeedID: &scope.feed11.Id})
|
||||||
|
have := getItemGuids(db.ListItems(model.ItemFilter{Status: &read}, 10, false, false))
|
||||||
want := []string{
|
want := []string{
|
||||||
"item111", "item112", "item122",
|
"item111", "item112", "item122",
|
||||||
"item211", "item012",
|
"item211", "item012",
|
||||||
|
|||||||
@@ -1,30 +1,32 @@
|
|||||||
package tests
|
package tests
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
"github.com/nkanaev/yarr/src/storage"
|
"github.com/nkanaev/yarr/src/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
func dbtest(t *testing.T, testcase func(t *testing.T, db storage.Storage)) {
|
func dbtest(t *testing.T, testcase func(t *testing.T, db storage.Storage)) {
|
||||||
|
t.Parallel()
|
||||||
testurls := map[string]string{
|
testurls := map[string]string{
|
||||||
"sqlite": ":memory:",
|
"sqlite": ":memory:",
|
||||||
}
|
}
|
||||||
|
|
||||||
if pgUrl := os.Getenv("YARR_POSTGRES_TEST_URL"); pgUrl != "" {
|
if pgImage := os.Getenv("YARR_POSTGRES_TEST_IMAGE"); pgImage != "" {
|
||||||
dburl, cleanup, err := createPostgresDB(pgUrl)
|
dburl, cleanup := startPostgresContainer(t, pgImage)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create postgres test database: %v", err)
|
|
||||||
}
|
|
||||||
t.Cleanup(cleanup)
|
t.Cleanup(cleanup)
|
||||||
testurls["postgres"] = dburl
|
testurls["postgres"] = dburl
|
||||||
|
} else if !testing.Short() {
|
||||||
|
t.Fatalf("YARR_POSTGRES_TEST_IMAGE not set; use -short to skip docker tests")
|
||||||
}
|
}
|
||||||
|
|
||||||
for testname, url := range testurls {
|
for testname, url := range testurls {
|
||||||
@@ -38,49 +40,72 @@ func dbtest(t *testing.T, testcase func(t *testing.T, db storage.Storage)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createPostgresDB(pgUrl string) (string, func(), error) {
|
func startPostgresContainer(t *testing.T, image string) (string, func()) {
|
||||||
u, err := url.Parse(pgUrl)
|
// database credentials
|
||||||
|
dbUser := "testuser"
|
||||||
|
dbPass := "password"
|
||||||
|
dbName := "yarrtest"
|
||||||
|
|
||||||
|
// generate unique container name
|
||||||
|
testHash := sha256.Sum256([]byte(t.Name()))
|
||||||
|
containerName := fmt.Sprintf("yarr-test-pg-%x-%d", testHash[:8], time.Now().UnixNano())
|
||||||
|
|
||||||
|
cmd := exec.Command(
|
||||||
|
"docker", "run", "-d", "--rm",
|
||||||
|
"--name", containerName,
|
||||||
|
"-p", "0:5432",
|
||||||
|
"-e", "POSTGRES_USER="+dbUser,
|
||||||
|
"-e", "POSTGRES_PASSWORD="+dbPass,
|
||||||
|
"-e", "POSTGRES_DB="+dbName,
|
||||||
|
image,
|
||||||
|
)
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
t.Fatalf("failed to start postgres container: %v\n%s", err, string(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
u.Path = "/postgres"
|
// retrieve the host port assigned by docker
|
||||||
adminConnStr := u.String()
|
portCmd := exec.Command("docker", "port", containerName, "5432/tcp")
|
||||||
|
portOut, err := portCmd.Output()
|
||||||
adminDB, err := sql.Open("postgres", adminConnStr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("admin connect: %w", err)
|
t.Fatalf("failed to get container port: %v", err)
|
||||||
}
|
}
|
||||||
|
parts := strings.Split(strings.TrimSpace(string(portOut)), ":")
|
||||||
|
dbPort := parts[len(parts)-1]
|
||||||
|
|
||||||
b := make([]byte, 4)
|
// build connection string
|
||||||
if _, err := rand.Read(b); err != nil {
|
pgUrl := fmt.Sprintf(
|
||||||
adminDB.Close()
|
"postgres://%s:%s@localhost:%s/%s?sslmode=disable",
|
||||||
return "", nil, fmt.Errorf("generate suffix: %w", err)
|
dbUser,
|
||||||
}
|
dbPass,
|
||||||
|
dbPort,
|
||||||
|
dbName,
|
||||||
|
)
|
||||||
|
|
||||||
testDBName := "yarr_test_" + hex.EncodeToString(b)
|
// wait up to 15 seconds for the container to accept connections
|
||||||
|
deadline := time.Now().Add(15 * time.Second)
|
||||||
if _, err := adminDB.Exec(fmt.Sprintf(`CREATE DATABASE "%s"`, testDBName)); err != nil {
|
for time.Now().Before(deadline) {
|
||||||
adminDB.Close()
|
db, err := sql.Open("postgres", pgUrl)
|
||||||
return "", nil, fmt.Errorf("create database: %w", err)
|
|
||||||
}
|
|
||||||
adminDB.Close()
|
|
||||||
|
|
||||||
u.Path = "/" + testDBName
|
|
||||||
testURL := u.String()
|
|
||||||
|
|
||||||
cleanup := func() {
|
|
||||||
dropDB, err := sql.Open("postgres", adminConnStr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
defer dropDB.Close()
|
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||||||
dropDB.Exec(fmt.Sprintf(
|
err = db.PingContext(ctx)
|
||||||
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '%s' AND pid <> pg_backend_pid()`,
|
cancel()
|
||||||
testDBName,
|
db.Close()
|
||||||
))
|
if err == nil {
|
||||||
dropDB.Exec(fmt.Sprintf(`DROP DATABASE IF EXISTS "%s"`, testDBName))
|
goto ready
|
||||||
}
|
}
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
}
|
||||||
|
t.Fatalf("timed out waiting for postgres container to be ready")
|
||||||
|
|
||||||
return testURL, cleanup, nil
|
ready:
|
||||||
|
// return connection url and a cleanup function that stops the container
|
||||||
|
return pgUrl, func() {
|
||||||
|
stop := exec.Command("docker", "stop", containerName)
|
||||||
|
if err := stop.Run(); err != nil {
|
||||||
|
t.Logf("failed to stop container %s: %v", containerName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user