Skip to content

Commit

Permalink
Added some tests and some code refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
CameronRP committed Apr 18, 2021
1 parent c7327f2 commit 1ae5a7d
Show file tree
Hide file tree
Showing 9 changed files with 379 additions and 163 deletions.
43 changes: 28 additions & 15 deletions audiobaitclient/audiobaitclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,46 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package audiobaitclient

import (
"encoding/json"
"errors"
"log"

"github.com/TheCacophonyProject/event-reporter/eventclient"
"github.com/godbus/dbus"
)

func Play(audioFileId, volume, priority int, makeEvent bool) (played bool, err error) {
data, err := dbusCall("Play", audioFileId, volume, priority, makeEvent)
// Can be mocked for testing
var dbusCall = func(method string, params ...interface{}) ([]interface{}, error) {
conn, err := dbus.SystemBus()
if err != nil {
return nil, err
}
obj := conn.Object("org.cacophony.Audiobait", "/org/cacophony/Audiobait")
call := obj.Call(method, 0, params...)
return call.Body, call.Err
}

var ErrorParsingOutput = errors.New("error with parsing dbus output")

func PlayFromId(audioFileId, volume, priority int, event *eventclient.Event) (played bool, err error) {
eventRaw := []byte{}
if event != nil {
eventRaw, err = json.Marshal(event)
if err != nil {
return false, err
}
}
log.Println(eventRaw)
data, err := dbusCall("PlayFromId", audioFileId, volume, priority, string(eventRaw))
if err != nil {
return false, err
}
if len(data) != 1 {
return false, errors.New("error playing sound")
return false, ErrorParsingOutput
}
played, ok := data[0].(bool)
if !ok {
return false, errors.New("error playing sound")
return false, ErrorParsingOutput
}
return played, nil

}

func dbusCall(method string, params ...interface{}) ([]interface{}, error) {
conn, err := dbus.SystemBus()
if err != nil {
return nil, err
}
obj := conn.Object("org.cacophony.Audiobait", "/org/cacophony/Audiobait")
call := obj.Call(method, 0, params...)
return call.Body, call.Err
}
53 changes: 53 additions & 0 deletions audiobaitclient/audiobaitclient_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package audiobaitclient

import (
"errors"
"testing"

"github.com/TheCacophonyProject/event-reporter/eventclient"
"github.com/stretchr/testify/assert"
)

func mockDBusCall(i []interface{}, err error) func(string, ...interface{}) ([]interface{}, error) {
return func(string, ...interface{}) ([]interface{}, error) {
return i, err
}
}

func TestPlayedAudio(t *testing.T) {
dbusCall = mockDBusCall([]interface{}{true}, nil)
played, err := PlayFromId(1, 2, 3, &eventclient.Event{})
assert.True(t, played)
assert.NoError(t, err)
}

func TestDidNotPlayAudio(t *testing.T) {
dbusCall = mockDBusCall([]interface{}{false}, nil)
played, err := PlayFromId(1, 2, 3, nil)
assert.False(t, played)
assert.NoError(t, err)
}

func TestPlayError(t *testing.T) {
dbusCall = mockDBusCall([]interface{}{true}, errors.New("an error"))
played, err := PlayFromId(1, 1, 1, &eventclient.Event{})
assert.False(t, played)
assert.Error(t, err)
}

func TestPlayBadDBusReturns(t *testing.T) {
dbusCall = mockDBusCall([]interface{}{1}, nil) // Returning wrong type
success, err := PlayFromId(1, 1, 1, &eventclient.Event{})
assert.False(t, success)
assert.Error(t, err)

dbusCall = mockDBusCall([]interface{}{true, true}, nil) // Returning too many
success, err = PlayFromId(1, 1, 1, &eventclient.Event{})
assert.False(t, success)
assert.Error(t, err)

dbusCall = mockDBusCall([]interface{}{}, nil) // Returning not enough
success, err = PlayFromId(1, 1, 1, &eventclient.Event{})
assert.False(t, success)
assert.Error(t, err)
}
15 changes: 7 additions & 8 deletions cmd/audiobait/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ func runMain() error {
return err
}

soundCard := NewSoundCardPlayer(conf.Card, conf.VolumeControl)

service, err := startService(conf.Dir, soundCard)
if err != nil {
if err := startService(player{
soundCard: NewSoundCardPlayer(conf.Card, conf.VolumeControl),
soundDir: conf.Dir,
}); err != nil {
return err
}
log.Println("started audiobait dbus servie")
Expand All @@ -93,16 +93,15 @@ func runMain() error {
var playTime <-chan time.Time
for {
log.Print("loading schedule from disk")
player, schedule, err := createPlayer(conf.Dir) //TODO add trigger output
service.setPlayer(player)
playlist, schedule, err := createPlayer(conf.Dir) //TODO add trigger output
if err != nil {
log.Printf("error creating player: %v (will wait for schedule update)", err)
playTime = nil
} else if len(schedule.Combos) < 1 {
log.Print("No schedule defined - waiting for schedule update")
playTime = nil
} else {
playIn := player.TimeUntilNextCombo(*schedule)
playIn := playlist.TimeUntilNextCombo(*schedule)
log.Printf("waiting %s for schedule to start", playIn)
playTime = time.After(playIn)
}
Expand All @@ -112,7 +111,7 @@ func runMain() error {
log.Print("new schedule - reloading")
case <-playTime:
log.Printf("Playing todays audiobait schedule...")
player.PlayTodaysSchedule(*schedule)
playlist.PlayTodaysSchedule(*schedule)
}
}
}
Expand Down
119 changes: 119 additions & 0 deletions cmd/audiobait/player.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
audiobait - play sounds to lure animals for The Cacophony Project API.
Copyright (C) 2018, The Cacophony Project
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package main

import (
"fmt"
"log"
"os/exec"
"time"

"github.com/TheCacophonyProject/audiobait/audiofilelibrary"
"github.com/TheCacophonyProject/event-reporter/eventclient"
)

type player struct {
soundCard SoundCardPlayer
soundDir string
}

// Can be mocked for testing
var saveEvent = eventclient.AddEvent
var openLibrary = audiofilelibrary.OpenLibrary
var now = time.Now

func (p *player) PlayFromId(fileId, volume, priority int, event *eventclient.Event) (bool, error) {
library, err := openLibrary(p.soundDir, scheduleFilename)
if err != nil {
return false, err
}
fileName, found := library.FilesByID[fileId]
if !found {
return false, fmt.Errorf("could not find file with ID %d", fileId)
}
log.Printf("playing '%s' at volume %d\n", fileName, volume)
playTime := now()
if err := p.soundCard.Play(p.soundDir+"/"+fileName, volume); err != nil {
return false, err
}
if event != nil {
if event.Type == "" {
event.Type = "audioBait"
}
event.Timestamp = playTime
if event.Details == nil {
event.Details = map[string]interface{}{}
}
event.Details["fileId"] = fileId
event.Details["volume"] = volume
event.Details["priority"] = priority
log.Println("saving audio event")
log.Println(event)
return true, saveEvent(*event)
}
return true, nil
}

type SoundCardPlayer interface {
Play(audioFileName string, volume int) error
}

// NewSoundCardPlayer constructs a new sound card player variable.
func NewSoundCardPlayer(aCard int, aControlName string) amixerPlayer {
return amixerPlayer{card: aCard, controlName: aControlName}
}

// amixerPlayer is a SoundCardPlayer that uses amixer.
type amixerPlayer struct {
card int
controlName string
}

// Play plays an audio file.
func (p amixerPlayer) Play(audioFileName string, volume int) error {
if err := p.setVolume(volume); err != nil {
return err
}
return p.play(audioFileName)
}

func (p *amixerPlayer) setVolume(volume int) error {
cmd := exec.Command(
"amixer",
"-c", fmt.Sprint(p.card),
"sset",
p.controlName,
fmt.Sprintf("%d%%", volume*10),
)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("volume set failed: %v\noutput:\n%s", err, out)
}
return nil
}

func (p *amixerPlayer) play(filename string) error {
cmd := exec.Command("play", "-q", filename)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("play failed: %v\noutput:\n%s", err, out)
}

return nil
}
103 changes: 103 additions & 0 deletions cmd/audiobait/player_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package main

import (
"errors"
"log"
"testing"
"time"

"github.com/TheCacophonyProject/audiobait/audiofilelibrary"
"github.com/TheCacophonyProject/event-reporter/eventclient"
"github.com/stretchr/testify/assert"
)

type mockSoundCard struct {
err error
}

func (fsc mockSoundCard) Play(audioFileName string, volume int) error {
return fsc.err
}

func newMockSoundCard(err error) mockSoundCard {
return mockSoundCard{
err: err,
}
}

func mockOpenLibrary(filesMap map[int]string, err error) {
openLibrary = func(soundDir, scheduleFilename string) (*audiofilelibrary.AudioFileLibrary, error) {
return &audiofilelibrary.AudioFileLibrary{
FilesByID: filesMap,
}, err
}
}

func mockSaveEvent(err error) **eventclient.Event {
var event *eventclient.Event
saveEvent = func(e eventclient.Event) error {
event = &e
return err
}
return &event
}

func TestPlayFromId(t *testing.T) {
newFakeNow()
log.Println("testing playing audio with event")
mockOpenLibrary(map[int]string{1: "a"}, nil)
event := mockSaveEvent(nil)
testPlayer := player{
soundCard: newMockSoundCard(nil),
}
played, err := testPlayer.PlayFromId(1, 2, 3, &eventclient.Event{})
assert.NoError(t, err)
assert.True(t, played)
assert.Equal(t, eventclient.Event{
Type: "audioBait",
Timestamp: now(),
Details: map[string]interface{}{
"fileId": 1,
"priority": 3,
"volume": 2,
},
}, **event)

log.Println("testing played audio with no event")
event = mockSaveEvent(nil)
played, err = testPlayer.PlayFromId(1, 2, 3, nil)
assert.NoError(t, err)
assert.True(t, played)
var expectedEvent *eventclient.Event
assert.Equal(t, expectedEvent, *event)

log.Println("testing failed library open")
libraryOpenFail := errors.New("failed to open library")
mockOpenLibrary(nil, libraryOpenFail)
played, err = testPlayer.PlayFromId(1, 2, 3, nil)
assert.Equal(t, libraryOpenFail, err)
assert.False(t, played)
assert.Equal(t, expectedEvent, *event)

log.Println("testing failed to find file in library")
mockOpenLibrary(map[int]string{1: "a"}, nil)
played, err = testPlayer.PlayFromId(2, 3, 4, nil)
assert.Error(t, err)
assert.False(t, played)
assert.Equal(t, expectedEvent, *event)

log.Println("testing failed to play audio")
soundcardError := errors.New("some soundcard error")
testPlayer.soundCard = newMockSoundCard(soundcardError)
played, err = testPlayer.PlayFromId(1, 2, 3, nil)
assert.False(t, played)
assert.Equal(t, soundcardError, err)
assert.Equal(t, expectedEvent, *event)
}

func newFakeNow() {
n := time.Now()
now = func() time.Time {
return n
}
}
Loading

0 comments on commit 1ae5a7d

Please sign in to comment.