split main, test parts

This commit is contained in:
Umputun 2019-01-09 01:51:55 -06:00
parent 8847871bf2
commit c5dc899751
5 changed files with 136 additions and 28 deletions

View file

@ -14,7 +14,7 @@ Use provided `docker-compose.yml` and change `FEED` value. All twitter-api crede
- `{{.Link}}` - rss link
- `{{.Text}}` - item description
_default is ` {{.Title}} - {{.Link}}`
_default is `{{.Title}} - {{.Link}}`_
## Parameters

View file

@ -3,6 +3,7 @@ package main
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"os/signal"
@ -19,15 +20,15 @@ import (
"github.com/umputun/rss2twitter/app/rss"
)
var opts struct {
type opts struct {
Refresh time.Duration `short:"r" long:"refresh" env:"REFRESH" default:"30s" description:"refresh interval"`
TimeOut time.Duration `short:"t" long:"timeout" env:"TIMEOUT" default:"5s" description:"rss feed timeout"`
Feed string `short:"f" long:"feed" env:"FEED" required:"true" description:"rss feed url"`
ConsumerKey string `long:"consumer-key" env:"TWI_CONSUMER_KEY" required:"true" description:"twitter consumer key"`
ConsumerSecret string `long:"consumer-secret" env:"TWI_CONSUMER_SECRET" required:"true" description:"twitter consumer secret"`
AccessToken string `long:"access-token" env:"TWI_ACCESS_TOKEN" required:"true" description:"twitter access token"`
AccessSecret string `long:"access-secret" env:"TWI_ACCESS_SECRET" required:"true" description:"twitter access secret"`
ConsumerKey string `long:"consumer-key" env:"TWI_CONSUMER_KEY" description:"twitter consumer key"`
ConsumerSecret string `long:"consumer-secret" env:"TWI_CONSUMER_SECRET" description:"twitter consumer secret"`
AccessToken string `long:"access-token" env:"TWI_ACCESS_TOKEN" description:"twitter access token"`
AccessSecret string `long:"access-secret" env:"TWI_ACCESS_SECRET" description:"twitter access secret"`
Template string `long:"template" env:"TEMPLATE" default:"{{.Title}} - {{.Link}}" description:"twitter message template"`
Dry bool `long:"dry" env:"DRY" description:"dry mode"`
@ -36,36 +37,59 @@ var opts struct {
var revision = "unknown"
type notifier interface {
Go(ctx context.Context) <-chan rss.Event
}
func main() {
fmt.Printf("rss2twitter - %s\n", revision)
if _, err := flags.Parse(&opts); err != nil {
o := opts{}
if _, err := flags.Parse(&o); err != nil {
os.Exit(1)
}
if opts.Dbg {
if o.Dbg {
log.Setup(log.Debug)
}
notifier := rss.Notify{Feed: opts.Feed, Duration: opts.Refresh, Timeout: opts.TimeOut}
var pub publisher.Interface = publisher.Twitter{
ConsumerKey: opts.ConsumerKey,
ConsumerSecret: opts.ConsumerSecret,
AccessToken: opts.AccessToken,
AccessSecret: opts.AccessSecret,
notifier, pub, err := setup(o)
if err != nil {
log.Printf("[PANIC] failed to setup, %v", err)
}
if opts.Dry { // override publisher to stdout only, no actual twitter publishing
pub = publisher.Stdout{}
do(context.Background(), notifier, pub, o.Template)
log.Print("[INFO] terminated")
}
func setup(o opts) (n notifier, p publisher.Interface, err error) {
n = &rss.Notify{Feed: o.Feed, Duration: o.Refresh, Timeout: o.TimeOut}
p = publisher.Twitter{
ConsumerKey: o.ConsumerKey,
ConsumerSecret: o.ConsumerSecret,
AccessToken: o.AccessToken,
AccessSecret: o.AccessSecret,
}
if o.Dry { // override publisher to stdout only, no actual twitter publishing
p = publisher.Stdout{}
log.Print("[INFO] dry mode")
} else {
if o.ConsumerKey == "" || o.ConsumerSecret == "" || o.AccessToken == "" || o.AccessSecret == "" {
return n, p, errors.New("token credentials missing")
}
}
return n, p, nil
}
log.Printf("[INFO] message template - %q", opts.Template)
ch := notifier.Go(context.Background())
// do runs event loop getting rss events and publishing them
func do(ctx context.Context, notif notifier, pub publisher.Interface, tmpl string) {
log.Printf("[INFO] message template - %q", tmpl)
ch := notif.Go(ctx)
for event := range ch {
err := pub.Publish(event, func(r rss.Event) string {
b1 := bytes.Buffer{}
if err := template.Must(template.New("twi").Parse(opts.Template)).Execute(&b1, event); err != nil {
if err := template.Must(template.New("twi").Parse(tmpl)).Execute(&b1, event); err != nil {
// template failed to parse record, backup predefined format
return fmt.Sprintf("%s - %s", r.Title, r.Link)
}
@ -75,7 +99,6 @@ func main() {
log.Printf("[WARN] failed to publish, %s", err)
}
}
log.Print("[INFO] terminated")
}
// format cleans text (removes html tags) and shrinks result

View file

@ -1,12 +1,67 @@
package main
import (
"bytes"
"context"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/umputun/rss2twitter/app/rss"
)
func TestSetupDry(t *testing.T) {
o := opts{Feed: "http://example.com", Dry: true}
n, p, err := setup(o)
require.NoError(t, err)
assert.NotNil(t, n)
assert.Equal(t, "publisher.Stdout", fmt.Sprintf("%T", p))
}
func TestSetupFull(t *testing.T) {
o := opts{Feed: "http://example.com", Dry: false,
ConsumerKey: "1", ConsumerSecret: "1", AccessToken: "1", AccessSecret: "1"}
n, p, err := setup(o)
require.NoError(t, err)
assert.NotNil(t, n)
assert.Equal(t, "publisher.Twitter", fmt.Sprintf("%T", p))
}
func TestSetupFailed(t *testing.T) {
o := opts{Feed: "http://example.com", Dry: false,
ConsumerKey: "1", ConsumerSecret: "1"}
_, _, err := setup(o)
assert.NotNil(t, err)
}
func TestDo(t *testing.T) {
pub := pubMock{buf: bytes.Buffer{}}
notif := notifierMock{delay: 100 * time.Millisecond, events: []rss.Event{
{GUID: "1", Title: "t1", Link: "l1", Text: "ttt2"},
{GUID: "2", Title: "t2", Link: "l2", Text: "ttt2"},
{GUID: "3", Title: "t4", Link: "l3", Text: "ttt3"},
}}
ctx, cancel := context.WithCancel(context.Background())
do(ctx, &notif, &pub, "{{.Title}} - {{.Link}}")
cancel()
assert.Equal(t, "t1 - l1\nt2 - l2\nt4 - l3\n", pub.buf.String())
}
func TestDoCanceled(t *testing.T) {
pub := pubMock{buf: bytes.Buffer{}}
notif := notifierMock{delay: 100 * time.Millisecond, events: []rss.Event{
{GUID: "1", Title: "t1", Link: "l1", Text: "ttt2"},
{GUID: "2", Title: "t2", Link: "l2", Text: "ttt2"},
{GUID: "3", Title: "t4", Link: "l3", Text: "ttt3"},
}}
ctx, cancel := context.WithCancel(context.Background())
time.AfterFunc(time.Millisecond*150, func() { cancel() })
do(ctx, &notif, &pub, "{{.Title}} - {{.Link}}")
assert.Equal(t, "t1 - l1\n", pub.buf.String())
}
func TestFormat(t *testing.T) {
tbl := []struct {
inp string
@ -27,3 +82,33 @@ func TestFormat(t *testing.T) {
})
}
}
type pubMock struct {
buf bytes.Buffer
}
func (m *pubMock) Publish(event rss.Event, formatter func(rss.Event) string) error {
_, err := m.buf.WriteString(formatter(event) + "\n")
return err
}
type notifierMock struct {
events []rss.Event
delay time.Duration
}
func (m *notifierMock) Go(ctx context.Context) <-chan rss.Event {
ch := make(chan rss.Event)
go func() {
for _, e := range m.events {
select {
case <-ctx.Done():
break
case <-time.After(m.delay):
ch <- e
}
}
close(ch)
}()
return ch
}

View file

@ -28,7 +28,7 @@ type Event struct {
Title string
Link string
Text string
guid string
GUID string
}
// Go starts notifier and returns events channel
@ -69,14 +69,14 @@ func (n *Notify) Go(ctx context.Context) <-chan Event {
continue
}
event, err := n.feedEvent(feedData)
if lastGUID != event.guid && err == nil {
if lastGUID != event.GUID && err == nil {
if lastGUID != "" { // don't notify on initial change
log.Printf("[INFO] new event %s - %s", event.guid, event.Title)
log.Printf("[INFO] new event %s - %s", event.GUID, event.Title)
ch <- event
} else {
log.Printf("[INFO] ignore first event %s - %s", event.guid, event.Title)
log.Printf("[INFO] ignore first event %s - %s", event.GUID, event.Title)
}
lastGUID = event.guid
lastGUID = event.GUID
}
if !waitOrCancel(n.ctx) {
log.Print("[WARN] notifier canceled")
@ -108,7 +108,7 @@ func (n *Notify) feedEvent(feed *gofeed.Feed) (e Event, err error) {
e.Title = feed.Items[0].Title
e.Link = feed.Items[0].Link
e.Text = "\n" + feed.Items[0].Description
e.guid = feed.Items[0].GUID
e.GUID = feed.Items[0].GUID
return e, nil
}

View file

@ -37,7 +37,7 @@ func TestNotify(t *testing.T) {
t.Logf("%+v", e)
e.Text = ""
assert.Equal(t, Event{ChanTitle: "Радио-Т", Title: "Радио-Т 626",
Link: "https://radio-t.com/p/2018/12/01/podcast-626/", guid: "https://radio-t.com/p/2018/12/01//podcast-626/"}, e)
Link: "https://radio-t.com/p/2018/12/01/podcast-626/", GUID: "https://radio-t.com/p/2018/12/01//podcast-626/"}, e)
assert.True(t, time.Since(st) >= time.Millisecond*250)
select {