github.com/diamondburned/arikawa/v2@v2.1.0/voice/session_test.go (about)

     1  package voice
     2  
     3  import (
     4  	"context"
     5  	"encoding/binary"
     6  	"io"
     7  	"log"
     8  	"os"
     9  	"runtime"
    10  	"strconv"
    11  	"sync"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/diamondburned/arikawa/v2/discord"
    16  	"github.com/diamondburned/arikawa/v2/internal/testenv"
    17  	"github.com/diamondburned/arikawa/v2/state"
    18  	"github.com/diamondburned/arikawa/v2/utils/wsutil"
    19  	"github.com/diamondburned/arikawa/v2/voice/voicegateway"
    20  	"github.com/pkg/errors"
    21  )
    22  
    23  func TestIntegration(t *testing.T) {
    24  	config := testenv.Must(t)
    25  
    26  	wsutil.WSDebug = func(v ...interface{}) {
    27  		_, file, line, _ := runtime.Caller(1)
    28  		caller := file + ":" + strconv.Itoa(line)
    29  		log.Println(append([]interface{}{caller}, v...)...)
    30  	}
    31  
    32  	s, err := state.New("Bot " + config.BotToken)
    33  	if err != nil {
    34  		t.Fatal("Failed to create a new state:", err)
    35  	}
    36  	AddIntents(s.Gateway)
    37  
    38  	if err := s.Open(); err != nil {
    39  		t.Fatal("Failed to connect:", err)
    40  	}
    41  	t.Cleanup(func() { s.Close() })
    42  
    43  	// Validate the given voice channel.
    44  	c, err := s.Channel(config.VoiceChID)
    45  	if err != nil {
    46  		t.Fatal("Failed to get channel:", err)
    47  	}
    48  	if c.Type != discord.GuildVoice {
    49  		t.Fatal("Channel isn't a guild voice channel.")
    50  	}
    51  
    52  	log.Println("The voice channel's name is", c.Name)
    53  
    54  	v, err := NewSession(s)
    55  	if err != nil {
    56  		t.Fatal("Failed to create a new voice session:", err)
    57  	}
    58  	v.ErrorLog = func(err error) { t.Error(err) }
    59  
    60  	// Grab a timer to benchmark things.
    61  	finish := timer()
    62  
    63  	// Add handler to receive speaking update beforehand.
    64  	v.AddHandler(func(e *voicegateway.SpeakingEvent) {
    65  		finish("receiving voice speaking event")
    66  	})
    67  
    68  	// Join the voice channel concurrently.
    69  	raceMe(t, "failed to join voice channel", func() (interface{}, error) {
    70  		return nil, v.JoinChannel(c.GuildID, c.ID, false, false)
    71  	})
    72  
    73  	t.Cleanup(func() {
    74  		log.Println("Leaving the voice channel concurrently.")
    75  
    76  		raceMe(t, "failed to leave voice channel", func() (interface{}, error) {
    77  			return nil, v.Leave()
    78  		})
    79  	})
    80  
    81  	finish("joining the voice channel")
    82  
    83  	// Create a context and only cancel it AFTER we're done sending silence
    84  	// frames.
    85  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    86  	t.Cleanup(cancel)
    87  
    88  	// Trigger speaking.
    89  	if err := v.Speaking(voicegateway.Microphone); err != nil {
    90  		t.Fatal("failed to start speaking:", err)
    91  	}
    92  
    93  	finish("sending the speaking command")
    94  
    95  	if err := v.UseContext(ctx); err != nil {
    96  		t.Fatal("failed to set ctx into vs:", err)
    97  	}
    98  
    99  	f, err := os.Open("testdata/nico.dca")
   100  	if err != nil {
   101  		t.Fatal("Failed to open nico.dca:", err)
   102  	}
   103  	defer f.Close()
   104  
   105  	var lenbuf [4]byte
   106  
   107  	// Copy the audio?
   108  	for {
   109  		if _, err := io.ReadFull(f, lenbuf[:]); err != nil {
   110  			if err == io.EOF {
   111  				break
   112  			}
   113  			t.Fatal("failed to read:", err)
   114  		}
   115  
   116  		// Read the integer
   117  		framelen := int64(binary.LittleEndian.Uint32(lenbuf[:]))
   118  
   119  		// Copy the frame.
   120  		if _, err := io.CopyN(v, f, framelen); err != nil && err != io.EOF {
   121  			t.Fatal("failed to write:", err)
   122  		}
   123  	}
   124  
   125  	finish("copying the audio")
   126  }
   127  
   128  // raceMe intentionally calls fn multiple times in goroutines to ensure it's not
   129  // racy.
   130  func raceMe(t *testing.T, wrapErr string, fn func() (interface{}, error)) interface{} {
   131  	const n = 3 // run 3 times
   132  	t.Helper()
   133  
   134  	// It is very ironic how this method itself is racy.
   135  
   136  	var wgr sync.WaitGroup
   137  	var mut sync.Mutex
   138  	var val interface{}
   139  	var err error
   140  
   141  	for i := 0; i < n; i++ {
   142  		wgr.Add(1)
   143  		go func() {
   144  			v, e := fn()
   145  
   146  			mut.Lock()
   147  			val = v
   148  			err = e
   149  			mut.Unlock()
   150  
   151  			if e != nil {
   152  				log.Println("Potential race test error:", e)
   153  			}
   154  
   155  			wgr.Done()
   156  		}()
   157  	}
   158  
   159  	wgr.Wait()
   160  
   161  	if err != nil {
   162  		t.Fatal("Race test failed:", errors.Wrap(err, wrapErr))
   163  	}
   164  
   165  	return val
   166  }
   167  
   168  // simple shitty benchmark thing
   169  func timer() func(finished string) {
   170  	var then = time.Now()
   171  
   172  	return func(finished string) {
   173  		now := time.Now()
   174  		log.Println("Finished", finished+", took", now.Sub(then))
   175  		then = now
   176  	}
   177  }