|
| 1 | +/* |
| 2 | +Copyright 2025 The Kubernetes Authors. |
| 3 | +
|
| 4 | +Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +you may not use this file except in compliance with the License. |
| 6 | +You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | +Unless required by applicable law or agreed to in writing, software |
| 11 | +distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +See the License for the specific language governing permissions and |
| 14 | +limitations under the License. |
| 15 | +*/ |
| 16 | + |
| 17 | +package content |
| 18 | + |
| 19 | +import ( |
| 20 | + "strconv" |
| 21 | + "strings" |
| 22 | + "testing" |
| 23 | +) |
| 24 | + |
| 25 | +func TestIsDecimalInteger(t *testing.T) { |
| 26 | + testCases := []struct { |
| 27 | + name string |
| 28 | + input string |
| 29 | + shouldPass bool |
| 30 | + errContains string |
| 31 | + }{ |
| 32 | + // valid |
| 33 | + {name: "zero", input: "0", shouldPass: true}, |
| 34 | + {name: "positive single digit 1", input: "1", shouldPass: true}, |
| 35 | + {name: "positive single digit 2", input: "2", shouldPass: true}, |
| 36 | + {name: "positive single digit 5", input: "5", shouldPass: true}, |
| 37 | + {name: "positive single digit 9", input: "9", shouldPass: true}, |
| 38 | + {name: "negative single digit", input: "-5", shouldPass: true}, |
| 39 | + {name: "negative single digit 1", input: "-1", shouldPass: true}, |
| 40 | + {name: "negative single digit 9", input: "-9", shouldPass: true}, |
| 41 | + |
| 42 | + {name: "number starting with 1", input: "100", shouldPass: true}, |
| 43 | + {name: "number starting with 2", input: "234", shouldPass: true}, |
| 44 | + {name: "number starting with 3", input: "345", shouldPass: true}, |
| 45 | + {name: "number starting with 4", input: "456", shouldPass: true}, |
| 46 | + {name: "number starting with 5", input: "567", shouldPass: true}, |
| 47 | + {name: "number starting with 6", input: "678", shouldPass: true}, |
| 48 | + {name: "number starting with 7", input: "789", shouldPass: true}, |
| 49 | + {name: "number starting with 8", input: "890", shouldPass: true}, |
| 50 | + {name: "number starting with 9", input: "999", shouldPass: true}, |
| 51 | + |
| 52 | + {name: "positive multi-digit", input: "123", shouldPass: true}, |
| 53 | + {name: "negative multi-digit", input: "-456", shouldPass: true}, |
| 54 | + {name: "negative starting with 1", input: "-100", shouldPass: true}, |
| 55 | + {name: "negative starting with 9", input: "-987", shouldPass: true}, |
| 56 | + {name: "large positive number", input: "9223372036854775807", shouldPass: true}, // max int64 |
| 57 | + {name: "large negative number", input: "-9223372036854775808", shouldPass: true}, // min int64 |
| 58 | + {name: "very long valid number", input: "12345678901234567890", shouldPass: true}, |
| 59 | + {name: "all nines", input: "999999999999", shouldPass: true}, |
| 60 | + |
| 61 | + // invalid |
| 62 | + {name: "negative zero", input: "-0", shouldPass: false}, |
| 63 | + {name: "double zero", input: "00", shouldPass: false}, |
| 64 | + {name: "triple zero", input: "000", shouldPass: false}, |
| 65 | + {name: "many zeros", input: "0000000", shouldPass: false}, |
| 66 | + {name: "leading zero single digit", input: "01", shouldPass: false}, |
| 67 | + {name: "leading zero digit 2", input: "02", shouldPass: false}, |
| 68 | + {name: "leading zero digit 9", input: "09", shouldPass: false}, |
| 69 | + {name: "leading zero multi-digit", input: "0123", shouldPass: false}, |
| 70 | + {name: "octal-like format", input: "0700", shouldPass: false}, |
| 71 | + {name: "octal-like format 2", input: "0950", shouldPass: false}, |
| 72 | + {name: "multiple leading zeros", input: "00123", shouldPass: false}, |
| 73 | + {name: "negative with leading zero", input: "-01", shouldPass: false}, |
| 74 | + {name: "negative with leading zeros", input: "-0123", shouldPass: false}, |
| 75 | + {name: "negative double zero", input: "-00", shouldPass: false}, |
| 76 | + {name: "plus sign", input: "+123", shouldPass: false}, |
| 77 | + {name: "positive plus sign", input: "+5", shouldPass: false}, |
| 78 | + {name: "plus zero", input: "+0", shouldPass: false}, |
| 79 | + |
| 80 | + // Invalid cases - empty and whitespace |
| 81 | + {name: "empty string", input: "", shouldPass: false, errContains: "non-empty"}, |
| 82 | + {name: "just minus sign", input: "-", shouldPass: false}, |
| 83 | + {name: "just plus sign", input: "+", shouldPass: false}, |
| 84 | + {name: "single space", input: " ", shouldPass: false}, |
| 85 | + {name: "multiple spaces", input: " ", shouldPass: false}, |
| 86 | + {name: "leading space", input: " 123", shouldPass: false}, |
| 87 | + {name: "trailing space", input: "123 ", shouldPass: false}, |
| 88 | + {name: "space in middle", input: "12 3", shouldPass: false}, |
| 89 | + {name: "spaces around", input: " 123 ", shouldPass: false}, |
| 90 | + |
| 91 | + {name: "decimal number", input: "12.3", shouldPass: false}, |
| 92 | + {name: "decimal zero", input: "0.0", shouldPass: false}, |
| 93 | + {name: "negative decimal", input: "-12.5", shouldPass: false}, |
| 94 | + {name: "trailing dot", input: "123.", shouldPass: false}, |
| 95 | + {name: "leading dot", input: ".123", shouldPass: false}, |
| 96 | + |
| 97 | + {name: "alphabetic", input: "abc", shouldPass: false}, |
| 98 | + {name: "alphanumeric", input: "12a3", shouldPass: false}, |
| 99 | + {name: "letter at start", input: "a123", shouldPass: false}, |
| 100 | + {name: "letter at end", input: "123a", shouldPass: false}, |
| 101 | + {name: "uppercase letters", input: "ABC", shouldPass: false}, |
| 102 | + {name: "mixed case", input: "12A3", shouldPass: false}, |
| 103 | + |
| 104 | + {name: "hexadecimal", input: "0x123", shouldPass: false}, |
| 105 | + {name: "hex uppercase", input: "0X123", shouldPass: false}, |
| 106 | + {name: "octal prefix", input: "0o777", shouldPass: false}, |
| 107 | + {name: "binary prefix", input: "0b101", shouldPass: false}, |
| 108 | + {name: "scientific notation", input: "1e5", shouldPass: false}, |
| 109 | + {name: "scientific negative exp", input: "1e-5", shouldPass: false}, |
| 110 | + {name: "scientific uppercase", input: "1E5", shouldPass: false}, |
| 111 | + |
| 112 | + {name: "underscore separator", input: "1_000", shouldPass: false}, |
| 113 | + {name: "comma separator", input: "1,000", shouldPass: false}, |
| 114 | + {name: "period separator", input: "1.000", shouldPass: false}, |
| 115 | + {name: "apostrophe separator", input: "1'000", shouldPass: false}, |
| 116 | + |
| 117 | + {name: "double minus", input: "--123", shouldPass: false}, |
| 118 | + {name: "double plus", input: "++123", shouldPass: false}, |
| 119 | + {name: "plus minus", input: "+-123", shouldPass: false}, |
| 120 | + {name: "minus plus", input: "-+123", shouldPass: false}, |
| 121 | + {name: "minus at end", input: "123-", shouldPass: false}, |
| 122 | + {name: "minus in middle", input: "12-3", shouldPass: false}, |
| 123 | + {name: "plus at end", input: "123+", shouldPass: false}, |
| 124 | + {name: "plus in middle", input: "12+3", shouldPass: false}, |
| 125 | + |
| 126 | + {name: "tab character at start", input: "\t123", shouldPass: false}, |
| 127 | + {name: "tab character at end", input: "123\t", shouldPass: false}, |
| 128 | + {name: "newline character", input: "123\n", shouldPass: false}, |
| 129 | + {name: "carriage return", input: "123\r", shouldPass: false}, |
| 130 | + {name: "null character", input: "123\x00", shouldPass: false}, |
| 131 | + {name: "vertical tab", input: "123\v", shouldPass: false}, |
| 132 | + {name: "form feed", input: "123\f", shouldPass: false}, |
| 133 | + |
| 134 | + {name: "parentheses", input: "(123)", shouldPass: false}, |
| 135 | + {name: "brackets", input: "[123]", shouldPass: false}, |
| 136 | + {name: "braces", input: "{123}", shouldPass: false}, |
| 137 | + {name: "dollar sign", input: "$123", shouldPass: false}, |
| 138 | + {name: "percent sign", input: "123%", shouldPass: false}, |
| 139 | + {name: "hash", input: "#123", shouldPass: false}, |
| 140 | + {name: "at sign", input: "@123", shouldPass: false}, |
| 141 | + {name: "ampersand", input: "&123", shouldPass: false}, |
| 142 | + {name: "asterisk", input: "*123", shouldPass: false}, |
| 143 | + {name: "slash", input: "12/3", shouldPass: false}, |
| 144 | + {name: "backslash", input: "12\\3", shouldPass: false}, |
| 145 | + {name: "pipe", input: "12|3", shouldPass: false}, |
| 146 | + {name: "semicolon", input: "12;3", shouldPass: false}, |
| 147 | + {name: "colon", input: "12:3", shouldPass: false}, |
| 148 | + {name: "question mark", input: "12?3", shouldPass: false}, |
| 149 | + {name: "exclamation", input: "12!3", shouldPass: false}, |
| 150 | + {name: "tilde", input: "~123", shouldPass: false}, |
| 151 | + {name: "backtick", input: "`123", shouldPass: false}, |
| 152 | + {name: "single quote", input: "'123'", shouldPass: false}, |
| 153 | + {name: "double quote", input: "\"123\"", shouldPass: false}, |
| 154 | + |
| 155 | + {name: "unicode minus", input: "−123", shouldPass: false}, // U+2212 minus sign |
| 156 | + {name: "unicode digit", input: "123", shouldPass: false}, // fullwidth digits |
| 157 | + {name: "arabic digits", input: "١٢٣", shouldPass: false}, // Arabic-Indic digits |
| 158 | + {name: "chinese characters", input: "一二三", shouldPass: false}, |
| 159 | + {name: "superscript", input: "123⁴", shouldPass: false}, |
| 160 | + {name: "subscript", input: "123₄", shouldPass: false}, |
| 161 | + } |
| 162 | + |
| 163 | + for _, tc := range testCases { |
| 164 | + t.Run(tc.name, func(t *testing.T) { |
| 165 | + errs := IsDecimalInteger(tc.input) |
| 166 | + if tc.shouldPass { |
| 167 | + if len(errs) != 0 { |
| 168 | + t.Errorf("IsDecimalInteger(%q) = %v, want no errors", tc.input, errs) |
| 169 | + } |
| 170 | + } else { |
| 171 | + if len(errs) == 0 { |
| 172 | + t.Errorf("IsDecimalInteger(%q) = no errors, want errors", tc.input) |
| 173 | + } else if tc.errContains != "" { |
| 174 | + found := false |
| 175 | + for _, err := range errs { |
| 176 | + if strings.Contains(err, tc.errContains) { |
| 177 | + found = true |
| 178 | + break |
| 179 | + } |
| 180 | + } |
| 181 | + if !found { |
| 182 | + t.Errorf("IsDecimalInteger(%q) errors %v should contain %q", tc.input, errs, tc.errContains) |
| 183 | + } |
| 184 | + } |
| 185 | + } |
| 186 | + }) |
| 187 | + } |
| 188 | + |
| 189 | + // Additional verification: valid strings should parse with strconv.ParseInt |
| 190 | + validCases := []string{ |
| 191 | + "0", "1", "2", "5", "9", |
| 192 | + "-1", "-5", "-9", |
| 193 | + "123", "-456", "100", "999", |
| 194 | + "9223372036854775807", "-9223372036854775808", |
| 195 | + "12345678901234567890", |
| 196 | + } |
| 197 | + for _, validCase := range validCases { |
| 198 | + if errs := IsDecimalInteger(validCase); len(errs) != 0 { |
| 199 | + t.Errorf("Valid case %q should return no errors, got: %v", validCase, errs) |
| 200 | + } |
| 201 | + // Verify it can also be parsed by strconv.ParseInt (within range) |
| 202 | + if len(validCase) <= 19 { // Only test cases that fit in int64 |
| 203 | + if _, err := strconv.ParseInt(validCase, 10, 64); err != nil { |
| 204 | + t.Errorf("Valid case %q should be parseable by strconv.ParseInt: %v", validCase, err) |
| 205 | + } |
| 206 | + } |
| 207 | + } |
| 208 | + |
| 209 | + // Verify that our function rejects what we intend to reject (even if strconv.ParseInt accepts it) |
| 210 | + rejectedCases := []string{ |
| 211 | + "0700", "0950", "01", "02", "09", |
| 212 | + "+123", "+5", "+0", |
| 213 | + "-0", "00", "000", |
| 214 | + "-01", "-00", |
| 215 | + } |
| 216 | + for _, rejectedCase := range rejectedCases { |
| 217 | + if errs := IsDecimalInteger(rejectedCase); len(errs) == 0 { |
| 218 | + t.Errorf("Case %q should be rejected by strict validation", rejectedCase) |
| 219 | + } |
| 220 | + } |
| 221 | + |
| 222 | + // Edge case: verify strconv.ParseInt accepts things we reject (proving we're stricter) |
| 223 | + strconvAcceptsButWeReject := []string{"+123", "0700", "01"} |
| 224 | + for _, case_ := range strconvAcceptsButWeReject { |
| 225 | + // strconv.ParseInt should accept it |
| 226 | + if _, err := strconv.ParseInt(case_, 10, 64); err != nil { |
| 227 | + t.Errorf("strconv.ParseInt should accept %q but got error: %v", case_, err) |
| 228 | + } |
| 229 | + // But our function should reject it |
| 230 | + if errs := IsDecimalInteger(case_); len(errs) == 0 { |
| 231 | + t.Errorf("IsDecimalInteger should reject %q (stricter than strconv)", case_) |
| 232 | + } |
| 233 | + } |
| 234 | +} |
0 commit comments