diff --git a/numcpus.go b/numcpus.go index af59983..de206f0 100644 --- a/numcpus.go +++ b/numcpus.go @@ -73,3 +73,26 @@ func GetPossible() (int, error) { func GetPresent() (int, error) { return getPresent() } + +// ListOffline returns the list of offline CPUs. See [GetOffline] for details on +// when a CPU is considered offline. +func ListOffline() ([]int, error) { + return listOffline() +} + +// ListOnline returns the list of CPUs that are online and being scheduled. +func ListOnline() ([]int, error) { + return listOnline() +} + +// ListPossible returns the list of possible CPUs. See [GetPossible] for +// details on when a CPU is considered possible. +func ListPossible() ([]int, error) { + return listPossible() +} + +// ListPresent returns the list of present CPUs. See [GetPresent] for +// details on when a CPU is considered present. +func ListPresent() ([]int, error) { + return listPresent() +} diff --git a/numcpus_linux.go b/numcpus_linux.go index af02a4f..c4b225d 100644 --- a/numcpus_linux.go +++ b/numcpus_linux.go @@ -83,6 +83,41 @@ func countCPURange(cpus string) (int, error) { return n, nil } +func listCPURange(cpus string) ([]int, error) { + // See comment in countCPURange. + if cpus == "" { + return []int{}, nil + } + + list := []int{} + for _, cpuRange := range strings.Split(cpus, ",") { + if cpuRange == "" { + return nil, fmt.Errorf("empty CPU range in CPU string %q", cpus) + } + from, to, found := strings.Cut(cpuRange, "-") + first, err := strconv.ParseUint(from, 10, 32) + if err != nil { + return nil, err + } + var last uint64 + if found { + last, err = strconv.ParseUint(to, 10, 32) + if err != nil { + return nil, err + } + } else { + last = first + } + if last < first { + return nil, fmt.Errorf("last CPU in range (%d) less than first (%d)", last, first) + } + for cpu := int(first); cpu <= int(last); cpu++ { + list = append(list, cpu) + } + } + return list, nil +} + func getConfigured() (int, error) { d, err := os.Open(sysfsCPUBasePath) if err != nil { @@ -135,3 +170,19 @@ func getPossible() (int, error) { func getPresent() (int, error) { return readCPURangeWith(present, countCPURange) } + +func listOffline() ([]int, error) { + return readCPURangeWith(offline, listCPURange) +} + +func listOnline() ([]int, error) { + return readCPURangeWith(online, listCPURange) +} + +func listPossible() ([]int, error) { + return readCPURangeWith(possible, listCPURange) +} + +func listPresent() ([]int, error) { + return readCPURangeWith(present, listCPURange) +} diff --git a/numcpus_linux_test.go b/numcpus_linux_test.go index e9624fa..f7cc9cd 100644 --- a/numcpus_linux_test.go +++ b/numcpus_linux_test.go @@ -14,50 +14,145 @@ package numcpus -import "testing" +import ( + "reflect" + "testing" +) -func TestCountCPURange(t *testing.T) { +func TestCPURange(t *testing.T) { testCases := []struct { - str string - n int - wantErr bool + str string + wantCount int + wantList []int + wantErr bool }{ - {str: "", n: 0}, - {str: "0", n: 1}, - {str: "0-1", n: 2}, - {str: "1-1", n: 1}, - {str: "0-7", n: 8}, - {str: "1-7", n: 7}, - {str: "1-15", n: 15}, - {str: "0-3,7", n: 5}, - {str: "0,2-4", n: 4}, - {str: "0,2-4,7", n: 5}, - {str: "0,2-4,7-15", n: 13}, - {str: "0,2-4,6,8-10", n: 8}, - {str: ",", wantErr: true}, - {str: "invalid", n: 0, wantErr: true}, - {str: "-", wantErr: true}, - {str: ",", wantErr: true}, - {str: ",1", wantErr: true}, - {str: "0,", wantErr: true}, - {str: "0-", wantErr: true}, - {str: "0,2-", wantErr: true}, - {str: "0-,1", wantErr: true}, - {str: "0,-3,5", wantErr: true}, - {str: "42-0", wantErr: true}, - {str: "0,5-3", wantErr: true}, + { + str: "", + wantCount: 0, + wantList: []int{}, + }, + { + str: "0", + wantCount: 1, + wantList: []int{0}, + }, + { + str: "0-1", + wantCount: 2, + wantList: []int{0, 1}, + }, + { + str: "1-1", + wantCount: 1, + wantList: []int{1}, + }, + { + str: "0-7", + wantCount: 8, + wantList: []int{0, 1, 2, 3, 4, 5, 6, 7}, + }, + { + str: "1-7", + wantCount: 7, + wantList: []int{1, 2, 3, 4, 5, 6, 7}, + }, + { + str: "1-15", + wantCount: 15, + wantList: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + }, + { + str: "0-3,7", + wantCount: 5, + wantList: []int{0, 1, 2, 3, 7}, + }, + { + str: "0,2-4", + wantCount: 4, + wantList: []int{0, 2, 3, 4}, + }, + { + str: "0,2-4,7", + wantCount: 5, + wantList: []int{0, 2, 3, 4, 7}, + }, + { + str: "0,2-4,7-15", + wantCount: 13, + wantList: []int{0, 2, 3, 4, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + }, + { + str: "0,2-4,6,8-10,31", + wantCount: 9, + wantList: []int{0, 2, 3, 4, 6, 8, 9, 10, 31}, + }, + { + str: "invalid", + wantErr: true, + }, + { + str: "-", + wantErr: true, + }, + { + str: ",", + wantErr: true, + }, + { + str: ",1", + wantErr: true, + }, + { + str: "0,", + wantErr: true, + }, + { + str: "0-", + wantErr: true, + }, + { + str: "-15", + wantErr: true, + }, + { + str: "0-,1", + wantErr: true, + }, + { + str: "0,-3,5", + wantErr: true, + }, + { + str: "42-0", + wantErr: true, + }, + { + str: "0,5-3", + wantErr: true, + }, } for _, tc := range testCases { - n, err := countCPURange(tc.str) + count, err := countCPURange(tc.str) if !tc.wantErr && err != nil { t.Errorf("countCPURange(%q) = %v, expected no error", tc.str, err) } else if tc.wantErr && err == nil { t.Errorf("countCPURange(%q) expected error", tc.str) } - if n != tc.n { - t.Errorf("countCPURange(%q) = %d, expected %d", tc.str, n, tc.n) + if count != tc.wantCount { + t.Errorf("countCPURange(%q) = %d, expected %d", tc.str, count, tc.wantCount) + } + + list, err := listCPURange(tc.str) + if !tc.wantErr && err != nil { + t.Errorf("listCPURange(%q) = %v, expected no error", tc.str, err) + } else if tc.wantErr && err == nil { + t.Errorf("listCPURange(%q) expected error", tc.str) + } + + if !reflect.DeepEqual(list, tc.wantList) { + t.Errorf("listCPURange(%q) = %d, expected %d", tc.str, list, tc.wantList) } } } diff --git a/numcpus_list_unsupported.go b/numcpus_list_unsupported.go new file mode 100644 index 0000000..af4efea --- /dev/null +++ b/numcpus_list_unsupported.go @@ -0,0 +1,33 @@ +// Copyright 2024 Tobias Klauser +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !linux + +package numcpus + +func listOffline() ([]int, error) { + return nil, ErrNotSupported +} + +func listOnline() ([]int, error) { + return nil, ErrNotSupported +} + +func listPossible() ([]int, error) { + return nil, ErrNotSupported +} + +func listPresent() ([]int, error) { + return nil, ErrNotSupported +} diff --git a/numcpus_test.go b/numcpus_test.go index 2b68d5a..236a836 100644 --- a/numcpus_test.go +++ b/numcpus_test.go @@ -82,44 +82,47 @@ func TestGetKernelMax(t *testing.T) { t.Logf("KernelMax = %v", n) } -func TestGetOffline(t *testing.T) { - n, err := numcpus.GetOffline() +func testNumAndList(t *testing.T, name string, get func() (int, error), list func() ([]int, error)) int { + t.Helper() + + n, err := get() if errors.Is(err, numcpus.ErrNotSupported) { - t.Skipf("GetOffline not supported on %s", runtime.GOOS) + t.Skipf("Get%s not supported on %s", name, runtime.GOOS) } else if err != nil { - t.Fatalf("GetOffline: %v", err) + t.Fatalf("Get%s: %v", name, err) } - t.Logf("Offline = %v", n) -} + t.Logf("%s = %v", name, n) -func TestGetOnline(t *testing.T) { - n, err := numcpus.GetOnline() + l, err := list() if errors.Is(err, numcpus.ErrNotSupported) { - t.Skipf("GetOnline not supported on %s", runtime.GOOS) + t.Skipf("List%s not supported on %s", name, runtime.GOOS) } else if err != nil { - t.Fatalf("GetOnline: %v", err) + t.Fatalf("List%s: %v", name, err) + } + t.Logf("List%s = %v", name, l) + + if len(l) != n { + t.Errorf("number of online CPUs in list %v doesn't match expected number of CPUs %d", l, n) } - t.Logf("Online = %v", n) + + return n +} + +func TestOffline(t *testing.T) { + testNumAndList(t, "Offline", numcpus.GetOffline, numcpus.ListOffline) +} + +func TestOnline(t *testing.T) { + n := testNumAndList(t, "Online", numcpus.GetOnline, numcpus.ListOnline) testGetconf(t, n, "GetOnline", confName("_NPROCESSORS_ONLN")) + } -func TestGetPossible(t *testing.T) { - n, err := numcpus.GetPossible() - if errors.Is(err, numcpus.ErrNotSupported) { - t.Skipf("GetPossible not supported on %s", runtime.GOOS) - } else if err != nil { - t.Fatalf("GetPossible: %v", err) - } - t.Logf("Possible = %v", n) +func TestPossible(t *testing.T) { + testNumAndList(t, "Possible", numcpus.GetPossible, numcpus.ListPossible) } -func TestGetPresent(t *testing.T) { - n, err := numcpus.GetPresent() - if errors.Is(err, numcpus.ErrNotSupported) { - t.Skipf("GetPresent not supported on %s", runtime.GOOS) - } else if err != nil { - t.Fatalf("GetPresent: %v", err) - } - t.Logf("Present = %v", n) +func TestPresent(t *testing.T) { + testNumAndList(t, "Present", numcpus.GetPresent, numcpus.ListPresent) }