mirror of
https://github.com/datarhei/core.git
synced 2025-10-06 08:27:08 +08:00
Make ffmpeg skills compareable
This commit is contained in:
@@ -169,7 +169,7 @@ func (m *manager) HTTPChallengeResolver(ctx context.Context, listenAddress strin
|
||||
// AcquireCertificates tries to acquire the certificates for the given hostnames synchronously.
|
||||
func (m *manager) AcquireCertificates(ctx context.Context, hostnames []string) error {
|
||||
m.lock.Lock()
|
||||
added, removed := slices.Diff(hostnames, m.hostnames)
|
||||
added, removed := slices.DiffComparable(hostnames, m.hostnames)
|
||||
m.lock.Unlock()
|
||||
|
||||
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.
|
||||
func (m *manager) ManageCertificates(ctx context.Context, hostnames []string) error {
|
||||
m.lock.Lock()
|
||||
added, removed := slices.Diff(hostnames, m.hostnames)
|
||||
added, removed := slices.DiffComparable(hostnames, m.hostnames)
|
||||
m.hostnames = make([]string, len(hostnames))
|
||||
copy(m.hostnames, hostnames)
|
||||
m.lock.Unlock()
|
||||
|
57
ffmpeg/skills/data.go
Normal file
57
ffmpeg/skills/data.go
Normal 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
|
||||
|
||||
`
|
@@ -11,6 +11,8 @@ import (
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/datarhei/core/v16/slices"
|
||||
)
|
||||
|
||||
// Codec represents a codec with its availabe encoders and decoders
|
||||
@@ -21,12 +23,48 @@ type Codec struct {
|
||||
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 {
|
||||
Audio []Codec
|
||||
Video []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)
|
||||
type HWDevice struct {
|
||||
Id string
|
||||
@@ -35,6 +73,26 @@ type HWDevice struct {
|
||||
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
|
||||
type Device struct {
|
||||
Id string
|
||||
@@ -42,11 +100,39 @@ type Device struct {
|
||||
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 {
|
||||
Demuxers []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)
|
||||
type Format struct {
|
||||
Id string
|
||||
@@ -58,6 +144,18 @@ type ffFormats struct {
|
||||
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)
|
||||
type Protocol struct {
|
||||
Id string
|
||||
@@ -69,6 +167,18 @@ type ffProtocols struct {
|
||||
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 {
|
||||
Id string
|
||||
Name string
|
||||
@@ -94,6 +204,26 @@ type ffmpeg struct {
|
||||
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
|
||||
type Skills struct {
|
||||
FFmpeg ffmpeg
|
||||
@@ -107,6 +237,38 @@ type Skills struct {
|
||||
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
|
||||
func New(binary string) (Skills, error) {
|
||||
c := Skills{}
|
||||
@@ -193,7 +355,7 @@ func filters(binary string) []Filter {
|
||||
func parseFilters(data []byte) []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.Split(bufio.ScanLines)
|
||||
@@ -227,7 +389,7 @@ func codecs(binary string) ffCodecs {
|
||||
func parseCodecs(data []byte) 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.Split(bufio.ScanLines)
|
||||
@@ -286,7 +448,7 @@ func formats(binary string) ffFormats {
|
||||
func parseFormats(data []byte) 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.Split(bufio.ScanLines)
|
||||
@@ -330,7 +492,7 @@ func devices(binary string) ffDevices {
|
||||
func parseDevices(data []byte, binary string) 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.Split(bufio.ScanLines)
|
||||
|
@@ -1,8 +1,10 @@
|
||||
package skills
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/datarhei/core/v16/slices"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -33,6 +35,70 @@ func TestNewInvalidBinary(t *testing.T) {
|
||||
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) {
|
||||
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)
|
||||
@@ -162,17 +228,7 @@ func TestMinorVersion(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCustomVersion(t *testing.T) {
|
||||
data := `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`
|
||||
data := ffmpegdata
|
||||
|
||||
f := parseVersion([]byte(data))
|
||||
|
||||
@@ -226,16 +282,7 @@ libpostproc 55. 9.100 / 55. 9.100`
|
||||
}
|
||||
|
||||
func TestFilters(t *testing.T) {
|
||||
data := ` ... 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.`
|
||||
data := filterdata
|
||||
|
||||
f := parseFilters([]byte(data))
|
||||
|
||||
@@ -284,10 +331,7 @@ func TestFilters(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 )
|
||||
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 )`
|
||||
data := codecdata
|
||||
|
||||
c := parseCodecs([]byte(data))
|
||||
|
||||
@@ -346,12 +390,7 @@ func TestCodecs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFormats(t *testing.T) {
|
||||
data := ` 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`
|
||||
data := formatdata
|
||||
|
||||
f := parseFormats([]byte(data))
|
||||
|
||||
@@ -396,15 +435,7 @@ func TestFormats(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProtocols(t *testing.T) {
|
||||
data := `Input:
|
||||
async
|
||||
bluray
|
||||
cache
|
||||
Output:
|
||||
crypto
|
||||
file
|
||||
ftp
|
||||
gopher`
|
||||
data := protocoldata
|
||||
|
||||
p := parseProtocols([]byte(data))
|
||||
|
||||
@@ -445,8 +476,7 @@ Output:
|
||||
}
|
||||
|
||||
func TestHWAccels(t *testing.T) {
|
||||
data := `Hardware acceleration methods:
|
||||
videotoolbox`
|
||||
data := hwacceldata
|
||||
|
||||
p := parseHWAccels([]byte(data))
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/datarhei/core/v16/slices"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -16,15 +17,9 @@ func TestNoV4LDevices(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestV4LDevices(t *testing.T) {
|
||||
data := bytes.NewBufferString(`mmal service 16.1 (platform:bcm2835-v4l2):
|
||||
/dev/video0
|
||||
data := v4ldata
|
||||
|
||||
Webcam C170: Webcam C170 (usb-3f980000.usb-1.3):
|
||||
/dev/video1
|
||||
|
||||
`)
|
||||
|
||||
devices := parseV4LDevices(data)
|
||||
devices := parseV4LDevices(bytes.NewBuffer(slices.Copy([]byte(data))))
|
||||
|
||||
require.Equal(t, []HWDevice{
|
||||
{
|
||||
|
@@ -6,3 +6,17 @@ func Copy[T any](src []T) []T {
|
||||
|
||||
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
|
||||
}
|
||||
|
@@ -13,3 +13,15 @@ func TestCopy(t *testing.T) {
|
||||
|
||||
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)
|
||||
}
|
||||
|
@@ -1,28 +1,81 @@
|
||||
package slices
|
||||
|
||||
// Diff returns a sliceof newly added entries and a slice of removed entries based
|
||||
// the provided slices.
|
||||
func Diff[T comparable](next, current []T) ([]T, []T) {
|
||||
added, removed := []T{}, []T{}
|
||||
// DiffComparable diffs two arrays/slices and returns slices of 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.
|
||||
// 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 {
|
||||
currentMap[name] = struct{}{}
|
||||
}
|
||||
|
||||
for _, name := range next {
|
||||
if _, ok := currentMap[name]; ok {
|
||||
delete(currentMap, name)
|
||||
// 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
|
||||
}
|
||||
|
||||
added = append(added, name)
|
||||
if listB[j] == element {
|
||||
visited[j] = true
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
extraA = append(extraA, element)
|
||||
}
|
||||
}
|
||||
|
||||
for name := range currentMap {
|
||||
removed = append(removed, name)
|
||||
for j := 0; j < bLen; j++ {
|
||||
if visited[j] {
|
||||
continue
|
||||
}
|
||||
extraB = append(extraB, listB[j])
|
||||
}
|
||||
|
||||
return added, removed
|
||||
return extraA, extraB
|
||||
}
|
||||
|
||||
// DiffEqualer diffs two arrays/slices where each element implements the Equaler interface and returns slices of
|
||||
// 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
|
||||
}
|
||||
|
@@ -6,12 +6,22 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDiff(t *testing.T) {
|
||||
func TestDiffComparable(t *testing.T) {
|
||||
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{"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
28
slices/equal.go
Normal 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
51
slices/equal_test.go
Normal 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)
|
||||
}
|
Reference in New Issue
Block a user