Make ffmpeg skills compareable

This commit is contained in:
Ingo Oppermann
2023-07-06 10:27:56 +02:00
parent 604893f8bb
commit 6c2e8b0ec3
11 changed files with 489 additions and 77 deletions

View File

@@ -169,7 +169,7 @@ func (m *manager) HTTPChallengeResolver(ctx context.Context, listenAddress strin
// AcquireCertificates tries to acquire the certificates for the given hostnames synchronously. // AcquireCertificates tries to acquire the certificates for the given hostnames synchronously.
func (m *manager) AcquireCertificates(ctx context.Context, hostnames []string) error { func (m *manager) AcquireCertificates(ctx context.Context, hostnames []string) error {
m.lock.Lock() m.lock.Lock()
added, removed := slices.Diff(hostnames, m.hostnames) added, removed := slices.DiffComparable(hostnames, m.hostnames)
m.lock.Unlock() m.lock.Unlock()
var err error var err error
@@ -202,7 +202,7 @@ func (m *manager) AcquireCertificates(ctx context.Context, hostnames []string) e
// ManageCertificates is the same as AcquireCertificates but it does it in the background. // ManageCertificates is the same as AcquireCertificates but it does it in the background.
func (m *manager) ManageCertificates(ctx context.Context, hostnames []string) error { func (m *manager) ManageCertificates(ctx context.Context, hostnames []string) error {
m.lock.Lock() m.lock.Lock()
added, removed := slices.Diff(hostnames, m.hostnames) added, removed := slices.DiffComparable(hostnames, m.hostnames)
m.hostnames = make([]string, len(hostnames)) m.hostnames = make([]string, len(hostnames))
copy(m.hostnames, hostnames) copy(m.hostnames, hostnames)
m.lock.Unlock() m.lock.Unlock()

57
ffmpeg/skills/data.go Normal file
View File

@@ -0,0 +1,57 @@
package skills
var ffmpegdata = `ffmpeg version 4.4.1-datarhei Copyright (c) 2000-2021 the FFmpeg developers
built with gcc 10.3.1 (Alpine 10.3.1_git20211027) 20211027
configuration: --extra-version=datarhei --prefix=/usr --extra-libs='-lpthread -lm -lz -lsupc++ -lstdc++ -lssl -lcrypto -lz -lc -ldl' --enable-nonfree --enable-gpl --enable-version3 --enable-postproc --enable-static --enable-openssl --enable-omx --enable-omx-rpi --enable-mmal --enable-v4l2_m2m --enable-libfreetype --enable-libsrt --enable-libx264 --enable-libx265 --enable-libvpx --enable-libmp3lame --enable-libopus --enable-libvorbis --disable-ffplay --disable-debug --disable-doc --disable-shared
libavutil 56. 70.100 / 56. 70.100
libavcodec 58.134.100 / 58.134.100
libavformat 58. 76.100 / 58. 76.100
libavdevice 58. 13.100 / 58. 13.100
libavfilter 7.110.100 / 7.110.100
libswscale 5. 9.100 / 5. 9.100
libswresample 3. 9.100 / 3. 9.100
libpostproc 55. 9.100 / 55. 9.100`
var filterdata = ` ... afirsrc |->A Generate a FIR coefficients audio stream.
... anoisesrc |->A Generate a noise audio signal.
... anullsrc |->A Null audio source, return empty audio frames.
... hilbert |->A Generate a Hilbert transform FIR coefficients.
... sinc |->A Generate a sinc kaiser-windowed low-pass, high-pass, band-pass, or band-reject FIR coefficients.
... sine |->A Generate sine wave audio signal.
... anullsink A->| Do absolutely nothing with the input audio.
... addroi V->V Add region of interest to frame.
... alphaextract V->N Extract an alpha channel as a grayscale image component.
T.. alphamerge VV->V Copy the luma value of the second input into the alpha channel of the first input.`
var codecdata = ` DEAIL. aac AAC (Advanced Audio Coding) (decoders: aac aac_fixed aac_at ) (encoders: aac aac_at )
DEVI.S y41p Uncompressed YUV 4:1:1 12-bit
DEV.LS h264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 (encoders: libx264 libx264rgb h264_videotoolbox )
DEV.L. flv1 FLV / Sorenson Spark / Sorenson H.263 (Flash Video) (decoders: flv ) (encoders: flv )`
var formatdata = ` DE mpeg MPEG-1 Systems / MPEG program stream
E mpeg1video raw MPEG-1 video
E mpeg2video raw MPEG-2 video
DE mpegts MPEG-TS (MPEG-2 Transport Stream)
D mpegtsraw raw MPEG-TS (MPEG-2 Transport Stream)
D mpegvideo raw MPEG video`
var protocoldata = `Input:
async
bluray
cache
Output:
crypto
file
ftp
gopher`
var hwacceldata = `Hardware acceleration methods:
videotoolbox`
var v4ldata = `mmal service 16.1 (platform:bcm2835-v4l2):
/dev/video0
Webcam C170: Webcam C170 (usb-3f980000.usb-1.3):
/dev/video1
`

View File

@@ -11,6 +11,8 @@ import (
"os/exec" "os/exec"
"regexp" "regexp"
"strings" "strings"
"github.com/datarhei/core/v16/slices"
) )
// Codec represents a codec with its availabe encoders and decoders // Codec represents a codec with its availabe encoders and decoders
@@ -21,12 +23,48 @@ type Codec struct {
Decoders []string Decoders []string
} }
func (a Codec) Equal(b Codec) bool {
if a.Id != b.Id {
return false
}
if a.Name != b.Name {
return false
}
if !slices.EqualComparableElements(a.Encoders, b.Encoders) {
return false
}
if !slices.EqualComparableElements(a.Decoders, b.Decoders) {
return false
}
return true
}
type ffCodecs struct { type ffCodecs struct {
Audio []Codec Audio []Codec
Video []Codec Video []Codec
Subtitle []Codec Subtitle []Codec
} }
func (a ffCodecs) Equal(b ffCodecs) bool {
if !slices.EqualEqualerElements(a.Audio, b.Audio) {
return false
}
if !slices.EqualEqualerElements(a.Video, b.Video) {
return false
}
if !slices.EqualEqualerElements(a.Subtitle, b.Subtitle) {
return false
}
return true
}
// HWDevice represents a hardware device (e.g. USB device) // HWDevice represents a hardware device (e.g. USB device)
type HWDevice struct { type HWDevice struct {
Id string Id string
@@ -35,6 +73,26 @@ type HWDevice struct {
Media string Media string
} }
func (a HWDevice) Equal(b HWDevice) bool {
if a.Id != b.Id {
return false
}
if a.Name != b.Name {
return false
}
if a.Extra != b.Extra {
return false
}
if a.Media != b.Media {
return false
}
return true
}
// Device represents a type of device (e.g. V4L2) including connected actual devices // Device represents a type of device (e.g. V4L2) including connected actual devices
type Device struct { type Device struct {
Id string Id string
@@ -42,11 +100,39 @@ type Device struct {
Devices []HWDevice Devices []HWDevice
} }
func (a Device) Equal(b Device) bool {
if a.Id != b.Id {
return false
}
if a.Name != b.Name {
return false
}
if !slices.EqualEqualerElements(a.Devices, b.Devices) {
return false
}
return true
}
type ffDevices struct { type ffDevices struct {
Demuxers []Device Demuxers []Device
Muxers []Device Muxers []Device
} }
func (a ffDevices) Equal(b ffDevices) bool {
if !slices.EqualEqualerElements(a.Demuxers, b.Demuxers) {
return false
}
if !slices.EqualEqualerElements(a.Muxers, b.Muxers) {
return false
}
return true
}
// Format represents a supported format (e.g. flv) // Format represents a supported format (e.g. flv)
type Format struct { type Format struct {
Id string Id string
@@ -58,6 +144,18 @@ type ffFormats struct {
Muxers []Format Muxers []Format
} }
func (a ffFormats) Equal(b ffFormats) bool {
if !slices.EqualComparableElements(a.Demuxers, b.Demuxers) {
return false
}
if !slices.EqualComparableElements(a.Muxers, b.Muxers) {
return false
}
return true
}
// Protocol represents a supported protocol (e.g. rtsp) // Protocol represents a supported protocol (e.g. rtsp)
type Protocol struct { type Protocol struct {
Id string Id string
@@ -69,6 +167,18 @@ type ffProtocols struct {
Output []Protocol Output []Protocol
} }
func (a ffProtocols) Equal(b ffProtocols) bool {
if !slices.EqualComparableElements(a.Input, b.Input) {
return false
}
if !slices.EqualComparableElements(a.Output, b.Output) {
return false
}
return true
}
type HWAccel struct { type HWAccel struct {
Id string Id string
Name string Name string
@@ -94,6 +204,26 @@ type ffmpeg struct {
Libraries []Library Libraries []Library
} }
func (a ffmpeg) Equal(b ffmpeg) bool {
if a.Version != b.Version {
return false
}
if a.Compiler != b.Compiler {
return false
}
if a.Configuration != b.Configuration {
return false
}
if !slices.EqualComparableElements(a.Libraries, b.Libraries) {
return false
}
return true
}
// Skills are the detected capabilities of a ffmpeg binary // Skills are the detected capabilities of a ffmpeg binary
type Skills struct { type Skills struct {
FFmpeg ffmpeg FFmpeg ffmpeg
@@ -107,6 +237,38 @@ type Skills struct {
Protocols ffProtocols Protocols ffProtocols
} }
func (a Skills) Equal(b Skills) bool {
if !a.FFmpeg.Equal(b.FFmpeg) {
return false
}
if !slices.EqualComparableElements(a.Filters, b.Filters) {
return false
}
if !slices.EqualComparableElements(a.HWAccels, b.HWAccels) {
return false
}
if !a.Codecs.Equal(b.Codecs) {
return false
}
if !a.Devices.Equal(b.Devices) {
return false
}
if !a.Formats.Equal(b.Formats) {
return false
}
if !a.Protocols.Equal(b.Protocols) {
return false
}
return true
}
// New returns all skills that ffmpeg provides // New returns all skills that ffmpeg provides
func New(binary string) (Skills, error) { func New(binary string) (Skills, error) {
c := Skills{} c := Skills{}
@@ -193,7 +355,7 @@ func filters(binary string) []Filter {
func parseFilters(data []byte) []Filter { func parseFilters(data []byte) []Filter {
filters := []Filter{} filters := []Filter{}
re := regexp.MustCompile(`^\s[TSC.]{3} ([0-9A-Za-z_]+)\s+(?:.*?)\s+(.*)?$`) re := regexp.MustCompile(`^\s*[TSC.]{3} ([0-9A-Za-z_]+)\s+(?:.*?)\s+(.*)?$`)
scanner := bufio.NewScanner(bytes.NewReader(data)) scanner := bufio.NewScanner(bytes.NewReader(data))
scanner.Split(bufio.ScanLines) scanner.Split(bufio.ScanLines)
@@ -227,7 +389,7 @@ func codecs(binary string) ffCodecs {
func parseCodecs(data []byte) ffCodecs { func parseCodecs(data []byte) ffCodecs {
codecs := ffCodecs{} codecs := ffCodecs{}
re := regexp.MustCompile(`^\s([D.])([E.])([VAS]).{3} ([0-9A-Za-z_]+)\s+(.*?)(?:\(decoders:([^\)]+)\))?\s?(?:\(encoders:([^\)]+)\))?$`) re := regexp.MustCompile(`^\s*([D.])([E.])([VAS]).{3} ([0-9A-Za-z_]+)\s+(.*?)(?:\(decoders:([^\)]+)\))?\s?(?:\(encoders:([^\)]+)\))?$`)
scanner := bufio.NewScanner(bytes.NewReader(data)) scanner := bufio.NewScanner(bytes.NewReader(data))
scanner.Split(bufio.ScanLines) scanner.Split(bufio.ScanLines)
@@ -286,7 +448,7 @@ func formats(binary string) ffFormats {
func parseFormats(data []byte) ffFormats { func parseFormats(data []byte) ffFormats {
formats := ffFormats{} formats := ffFormats{}
re := regexp.MustCompile(`^\s([D ])([E ]) ([0-9A-Za-z_,]+)\s+(.*?)$`) re := regexp.MustCompile(`^\s*([D ])([E ])\s+([0-9A-Za-z_,]+)\s+(.*?)$`)
scanner := bufio.NewScanner(bytes.NewReader(data)) scanner := bufio.NewScanner(bytes.NewReader(data))
scanner.Split(bufio.ScanLines) scanner.Split(bufio.ScanLines)
@@ -330,7 +492,7 @@ func devices(binary string) ffDevices {
func parseDevices(data []byte, binary string) ffDevices { func parseDevices(data []byte, binary string) ffDevices {
devices := ffDevices{} devices := ffDevices{}
re := regexp.MustCompile(`^\s([D ])([E ]) ([0-9A-Za-z_,]+)\s+(.*?)$`) re := regexp.MustCompile(`^\s*([D ])([E ]) ([0-9A-Za-z_,]+)\s+(.*?)$`)
scanner := bufio.NewScanner(bytes.NewReader(data)) scanner := bufio.NewScanner(bytes.NewReader(data))
scanner.Split(bufio.ScanLines) scanner.Split(bufio.ScanLines)

View File

@@ -1,8 +1,10 @@
package skills package skills
import ( import (
"bytes"
"testing" "testing"
"github.com/datarhei/core/v16/slices"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -33,6 +35,70 @@ func TestNewInvalidBinary(t *testing.T) {
require.Empty(t, skills.Protocols.Output) require.Empty(t, skills.Protocols.Output)
} }
func TestEqualEmptySkills(t *testing.T) {
s := Skills{}
ok := s.Equal(s)
require.True(t, ok)
}
func TestEuqalSkills(t *testing.T) {
s1 := Skills{
FFmpeg: parseVersion([]byte(ffmpegdata)),
Filters: parseFilters([]byte(filterdata)),
HWAccels: parseHWAccels([]byte(hwacceldata)),
Codecs: parseCodecs([]byte(codecdata)),
Devices: ffDevices{},
Formats: parseFormats([]byte(formatdata)),
Protocols: parseProtocols([]byte(protocoldata)),
}
devices := parseV4LDevices(bytes.NewBuffer(slices.Copy([]byte(v4ldata))))
s1.Devices.Demuxers = append(s1.Devices.Demuxers, Device{
Id: "v4l2",
Name: "webcam",
Devices: devices,
})
s1.Devices.Muxers = append(s1.Devices.Muxers, Device{
Id: "v4l2",
Name: "webcam",
Devices: devices,
})
ok := s1.Equal(s1)
require.True(t, ok)
s2 := Skills{
FFmpeg: parseVersion([]byte(ffmpegdata)),
Filters: parseFilters([]byte(filterdata)),
HWAccels: parseHWAccels([]byte(hwacceldata)),
Codecs: parseCodecs([]byte(codecdata)),
Devices: ffDevices{},
Formats: parseFormats([]byte(formatdata)),
Protocols: parseProtocols([]byte(protocoldata)),
}
devices = parseV4LDevices(bytes.NewBuffer(slices.Copy([]byte(v4ldata))))
s2.Devices.Demuxers = append(s2.Devices.Demuxers, Device{
Id: "v4l2",
Name: "webcam",
Devices: devices,
})
s2.Devices.Muxers = append(s2.Devices.Muxers, Device{
Id: "v4l2",
Name: "webcam",
Devices: devices,
})
ok = s1.Equal(s2)
require.True(t, ok)
ok = s1.Equal(Skills{})
require.False(t, ok)
}
func TestPatchVersion(t *testing.T) { func TestPatchVersion(t *testing.T) {
data := `ffmpeg version 4.3.1 Copyright (c) 2000-2020 the FFmpeg developers data := `ffmpeg version 4.3.1 Copyright (c) 2000-2020 the FFmpeg developers
built with Apple clang version 12.0.0 (clang-1200.0.32.29) built with Apple clang version 12.0.0 (clang-1200.0.32.29)
@@ -162,17 +228,7 @@ func TestMinorVersion(t *testing.T) {
} }
func TestCustomVersion(t *testing.T) { func TestCustomVersion(t *testing.T) {
data := `ffmpeg version 4.4.1-datarhei Copyright (c) 2000-2021 the FFmpeg developers data := ffmpegdata
built with gcc 10.3.1 (Alpine 10.3.1_git20211027) 20211027
configuration: --extra-version=datarhei --prefix=/usr --extra-libs='-lpthread -lm -lz -lsupc++ -lstdc++ -lssl -lcrypto -lz -lc -ldl' --enable-nonfree --enable-gpl --enable-version3 --enable-postproc --enable-static --enable-openssl --enable-omx --enable-omx-rpi --enable-mmal --enable-v4l2_m2m --enable-libfreetype --enable-libsrt --enable-libx264 --enable-libx265 --enable-libvpx --enable-libmp3lame --enable-libopus --enable-libvorbis --disable-ffplay --disable-debug --disable-doc --disable-shared
libavutil 56. 70.100 / 56. 70.100
libavcodec 58.134.100 / 58.134.100
libavformat 58. 76.100 / 58. 76.100
libavdevice 58. 13.100 / 58. 13.100
libavfilter 7.110.100 / 7.110.100
libswscale 5. 9.100 / 5. 9.100
libswresample 3. 9.100 / 3. 9.100
libpostproc 55. 9.100 / 55. 9.100`
f := parseVersion([]byte(data)) f := parseVersion([]byte(data))
@@ -226,16 +282,7 @@ libpostproc 55. 9.100 / 55. 9.100`
} }
func TestFilters(t *testing.T) { func TestFilters(t *testing.T) {
data := ` ... afirsrc |->A Generate a FIR coefficients audio stream. data := filterdata
... anoisesrc |->A Generate a noise audio signal.
... anullsrc |->A Null audio source, return empty audio frames.
... hilbert |->A Generate a Hilbert transform FIR coefficients.
... sinc |->A Generate a sinc kaiser-windowed low-pass, high-pass, band-pass, or band-reject FIR coefficients.
... sine |->A Generate sine wave audio signal.
... anullsink A->| Do absolutely nothing with the input audio.
... addroi V->V Add region of interest to frame.
... alphaextract V->N Extract an alpha channel as a grayscale image component.
T.. alphamerge VV->V Copy the luma value of the second input into the alpha channel of the first input.`
f := parseFilters([]byte(data)) f := parseFilters([]byte(data))
@@ -284,10 +331,7 @@ func TestFilters(t *testing.T) {
} }
func TestCodecs(t *testing.T) { func TestCodecs(t *testing.T) {
data := ` DEAIL. aac AAC (Advanced Audio Coding) (decoders: aac aac_fixed aac_at ) (encoders: aac aac_at ) data := codecdata
DEVI.S y41p Uncompressed YUV 4:1:1 12-bit
DEV.LS h264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 (encoders: libx264 libx264rgb h264_videotoolbox )
DEV.L. flv1 FLV / Sorenson Spark / Sorenson H.263 (Flash Video) (decoders: flv ) (encoders: flv )`
c := parseCodecs([]byte(data)) c := parseCodecs([]byte(data))
@@ -346,12 +390,7 @@ func TestCodecs(t *testing.T) {
} }
func TestFormats(t *testing.T) { func TestFormats(t *testing.T) {
data := ` DE mpeg MPEG-1 Systems / MPEG program stream data := formatdata
E mpeg1video raw MPEG-1 video
E mpeg2video raw MPEG-2 video
DE mpegts MPEG-TS (MPEG-2 Transport Stream)
D mpegtsraw raw MPEG-TS (MPEG-2 Transport Stream)
D mpegvideo raw MPEG video`
f := parseFormats([]byte(data)) f := parseFormats([]byte(data))
@@ -396,15 +435,7 @@ func TestFormats(t *testing.T) {
} }
func TestProtocols(t *testing.T) { func TestProtocols(t *testing.T) {
data := `Input: data := protocoldata
async
bluray
cache
Output:
crypto
file
ftp
gopher`
p := parseProtocols([]byte(data)) p := parseProtocols([]byte(data))
@@ -445,8 +476,7 @@ Output:
} }
func TestHWAccels(t *testing.T) { func TestHWAccels(t *testing.T) {
data := `Hardware acceleration methods: data := hwacceldata
videotoolbox`
p := parseHWAccels([]byte(data)) p := parseHWAccels([]byte(data))

View File

@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"testing" "testing"
"github.com/datarhei/core/v16/slices"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -16,15 +17,9 @@ func TestNoV4LDevices(t *testing.T) {
} }
func TestV4LDevices(t *testing.T) { func TestV4LDevices(t *testing.T) {
data := bytes.NewBufferString(`mmal service 16.1 (platform:bcm2835-v4l2): data := v4ldata
/dev/video0
Webcam C170: Webcam C170 (usb-3f980000.usb-1.3): devices := parseV4LDevices(bytes.NewBuffer(slices.Copy([]byte(data))))
/dev/video1
`)
devices := parseV4LDevices(data)
require.Equal(t, []HWDevice{ require.Equal(t, []HWDevice{
{ {

View File

@@ -6,3 +6,17 @@ func Copy[T any](src []T) []T {
return dst return dst
} }
type Cloner[T any] interface {
Clone() T
}
func CopyDeep[T any, X Cloner[T]](src []X) []T {
dst := make([]T, len(src))
for i, c := range src {
dst[i] = c.Clone()
}
return dst
}

View File

@@ -13,3 +13,15 @@ func TestCopy(t *testing.T) {
require.Equal(t, []string{"a", "b", "c"}, b) require.Equal(t, []string{"a", "b", "c"}, b)
} }
func (a String) Clone() String {
return String(string(a))
}
func TestCopyDeep(t *testing.T) {
a := []String{"a", "b", "c"}
b := CopyDeep[String](a)
require.Equal(t, []String{"a", "b", "c"}, b)
}

View File

@@ -1,28 +1,81 @@
package slices package slices
// Diff returns a sliceof newly added entries and a slice of removed entries based // DiffComparable diffs two arrays/slices and returns slices of elements that are only in A and only in B.
// the provided slices. // If some element is present multiple times, each instance is counted separately (e.g. if something is 2x in A and
func Diff[T comparable](next, current []T) ([]T, []T) { // 5x in B, it will be 0x in extraA and 3x in extraB). The order of items in both lists is ignored.
added, removed := []T{}, []T{} // Adapted from https://github.com/stretchr/testify/blob/f97607b89807936ac4ff96748d766cf4b9711f78/assert/assertions.go#L1073C21-L1073C21
func DiffComparable[T comparable](listA, listB []T) ([]T, []T) {
extraA, extraB := []T{}, []T{}
currentMap := map[T]struct{}{} aLen := len(listA)
bLen := len(listB)
for _, name := range current { // Mark indexes in listA that we already used
currentMap[name] = struct{}{} visited := make([]bool, bLen)
for i := 0; i < aLen; i++ {
element := listA[i]
found := false
for j := 0; j < bLen; j++ {
if visited[j] {
continue
}
if listB[j] == element {
visited[j] = true
found = true
break
}
}
if !found {
extraA = append(extraA, element)
}
} }
for _, name := range next { for j := 0; j < bLen; j++ {
if _, ok := currentMap[name]; ok { if visited[j] {
delete(currentMap, name)
continue continue
} }
extraB = append(extraB, listB[j])
added = append(added, name)
} }
for name := range currentMap { return extraA, extraB
removed = append(removed, name) }
}
// DiffEqualer diffs two arrays/slices where each element implements the Equaler interface and returns slices of
return added, removed // elements that are only in A and only in B. If some element is present multiple times, each instance is counted
// separately (e.g. if something is 2x in A and 5x in B, it will be 0x in extraA and 3x in extraB). The order of
// items in both lists is ignored.
func DiffEqualer[T any, X Equaler[T]](listA []T, listB []X) ([]T, []X) {
extraA, extraB := []T{}, []X{}
aLen := len(listA)
bLen := len(listB)
// Mark indexes in listA that we already used
visited := make([]bool, bLen)
for i := 0; i < aLen; i++ {
element := listA[i]
found := false
for j := 0; j < bLen; j++ {
if visited[j] {
continue
}
if listB[j].Equal(element) {
visited[j] = true
found = true
break
}
}
if !found {
extraA = append(extraA, element)
}
}
for j := 0; j < bLen; j++ {
if visited[j] {
continue
}
extraB = append(extraB, listB[j])
}
return extraA, extraB
} }

View File

@@ -6,12 +6,22 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestDiff(t *testing.T) { func TestDiffComparable(t *testing.T) {
a := []string{"c", "d", "e", "f"} a := []string{"c", "d", "e", "f"}
b := []string{"a", "b", "c", "d"} b := []string{"a", "a", "b", "c", "d"}
added, removed := Diff(a, b) added, removed := DiffComparable(a, b)
require.ElementsMatch(t, []string{"e", "f"}, added) require.ElementsMatch(t, []string{"e", "f"}, added)
require.ElementsMatch(t, []string{"a", "b"}, removed) require.ElementsMatch(t, []string{"a", "a", "b"}, removed)
}
func TestDiffEqualer(t *testing.T) {
a := []String{"c", "d", "e", "f"}
b := []String{"a", "a", "b", "c", "d"}
added, removed := DiffComparable(a, b)
require.ElementsMatch(t, []String{"e", "f"}, added)
require.ElementsMatch(t, []String{"a", "a", "b"}, removed)
} }

28
slices/equal.go Normal file
View File

@@ -0,0 +1,28 @@
package slices
// EqualComparableElements returns whether two slices have the same elements.
func EqualComparableElements[T comparable](a, b []T) bool {
extraA, extraB := DiffComparable(a, b)
if len(extraA) == 0 && len(extraB) == 0 {
return true
}
return false
}
// Equaler defines a type that implements the Equal function.
type Equaler[T any] interface {
Equal(T) bool
}
// EqualEqualerElements returns whether two slices of Equaler have the same elements.
func EqualEqualerElements[T any, X Equaler[T]](a []T, b []X) bool {
extraA, extraB := DiffEqualer(a, b)
if len(extraA) == 0 && len(extraB) == 0 {
return true
}
return false
}

51
slices/equal_test.go Normal file
View File

@@ -0,0 +1,51 @@
package slices
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestEqualComparableElements(t *testing.T) {
a := []string{"a", "b", "c", "d"}
b := []string{"b", "c", "a", "d"}
ok := EqualComparableElements(a, b)
require.True(t, ok)
ok = EqualComparableElements(b, a)
require.True(t, ok)
a = append(a, "z")
ok = EqualComparableElements(a, b)
require.False(t, ok)
ok = EqualComparableElements(b, a)
require.False(t, ok)
}
type String string
func (a String) Equal(b String) bool {
return string(a) == string(b)
}
func TestEqualEqualerElements(t *testing.T) {
a := []String{"a", "b", "c", "d"}
b := []String{"b", "c", "a", "d"}
ok := EqualEqualerElements(a, b)
require.True(t, ok)
ok = EqualEqualerElements(b, a)
require.True(t, ok)
a = append(a, "z")
ok = EqualEqualerElements(a, b)
require.False(t, ok)
ok = EqualEqualerElements(b, a)
require.False(t, ok)
}