diff --git a/.ci/check-whitespace.jl b/.ci/check-whitespace.jl new file mode 100755 index 00000000..604af8dc --- /dev/null +++ b/.ci/check-whitespace.jl @@ -0,0 +1,98 @@ +#!/usr/bin/env julia + +const patterns = split(""" + *.jl + *.md + *.yml + *Makefile +""") + +const is_gha = something(tryparse(Bool, get(ENV, "GITHUB_ACTIONS", "false")), false) + +# Note: `git ls-files` gives `/` as a path separator on Windows, +# so we just use `/` for all platforms. +allow_tabs(path) = + endswith(path, "Makefile") || + endswith(path, ".make") || + endswith(path, ".mk") + +function check_whitespace() + errors = Set{Tuple{String,Int,String}}() + files_to_check = filter(arg -> !startswith(arg, "-"), ARGS) + if isempty(files_to_check) + if "--stdin" in ARGS + files_to_check = collect(eachline(stdin)) + else + files_to_check = collect(eachline(`git ls-files -- $patterns`)) + end + end + + files_fixed = 0 + if "--fix" in ARGS + for path in files_to_check + content = newcontent = read(path, String) + isempty(content) && continue + if !allow_tabs(path) + tabpattern = r"^([ \t]+)"m => (x -> replace(x, r"((?: {4})*)( *\t)" => s"\1 ")) # Replace tab sequences at start of line after any number of 4-space groups + newcontent = replace(newcontent, tabpattern) + end + newcontent = replace(newcontent, + r"\s*$" => '\n', # Remove trailing whitespace and normalize line ending at eof + r"\s*?[\r\n]" => '\n', # Remove trailing whitespace and normalize line endings on each line + r"\xa0" => ' ' # Replace non-breaking spaces + ) + if content != newcontent + write(path, newcontent) + files_fixed += 1 + end + end + if files_fixed > 0 + println(stderr, "Fixed whitespace issues in $files_fixed files.") + end + end + + for path in files_to_check + lineno = 0 + non_blank = 0 + + file_err(msg) = push!(errors, (path, 0, msg)) + line_err(msg) = push!(errors, (path, lineno, msg)) + + isfile(path) || continue + for line in eachline(path, keep=true) + lineno += 1 + contains(line, '\r') && file_err("non-UNIX line endings") + contains(line, '\ua0') && line_err("non-breaking space") + allow_tabs(path) || + contains(line, '\t') && line_err("tab") + endswith(line, '\n') || line_err("no trailing newline") + line = chomp(line) + endswith(line, r"\s") && line_err("trailing whitespace") + contains(line, r"\S") && (non_blank = lineno) + end + non_blank < lineno && line_err("trailing blank lines") + end + + if isempty(errors) + println(stderr, "Whitespace check found no issues.") + exit(0) + else + println(stderr, "Whitespace check found $(length(errors)) issues:") + for (path, lineno, msg) in sort!(collect(errors)) + if lineno == 0 + println(stderr, "$path -- $msg") + if is_gha + println(stdout, "::warning title=Whitespace check,file=", path, "::", msg) + end + else + println(stderr, "$path:$lineno -- $msg") + if is_gha + println(stdout, "::warning title=Whitespace check,file=", path, ",line=", lineno, "::", msg) + end + end + end + exit(1) + end +end + +check_whitespace() diff --git a/.github/workflows/Whitespace.yml b/.github/workflows/Whitespace.yml new file mode 100644 index 00000000..3931cba2 --- /dev/null +++ b/.github/workflows/Whitespace.yml @@ -0,0 +1,26 @@ +name: Whitespace + +permissions: {} + +on: + push: + branches: + - main + pull_request: + +jobs: + whitespace: + name: Check whitespace + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - name: Checkout the JuliaSparse/SparseArrays.jl repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: julia-actions/setup-julia@f6f565d9f7cf12f53dc8045742460d6260ad3b39 # v3.0.1 + with: + version: '1.11.6' + - name: Check whitespace + run: | + julia .ci/check-whitespace.jl diff --git a/gen/Makefile b/gen/Makefile index e19103fc..132889fc 100755 --- a/gen/Makefile +++ b/gen/Makefile @@ -7,7 +7,7 @@ all: clean download julia --project generator.jl ./SuiteSparse-$(VER) clean: - rm -fr *.tar.gz SuiteSparse* + rm -fr *.tar.gz SuiteSparse* download: curl -L -O https://github.com/JuliaBinaryWrappers/SuiteSparse_jll.jl/releases/download/SuiteSparse-v$(VER)%2B0/SuiteSparse.v$(VER).x86_64-linux-gnu.tar.gz diff --git a/src/SparseArrays.jl b/src/SparseArrays.jl index 46b9c01c..2418f9e2 100644 --- a/src/SparseArrays.jl +++ b/src/SparseArrays.jl @@ -85,7 +85,7 @@ if Base.USE_GPL_LIBS include("solvers/spqr.jl") end -zero(a::AbstractSparseArray) = spzeros(eltype(a), size(a)...) +zero(a::AbstractSparseArray{Tv,Ti}) where {Tv,Ti} = spzeros(Tv, Ti, size(a)...) LinearAlgebra.diagzero(D::Diagonal{<:AbstractSparseMatrix{T}},i,j) where {T} = spzeros(T, size(D.diag[i], 1), size(D.diag[j], 2)) diff --git a/src/solvers/cholmod.jl b/src/solvers/cholmod.jl index 504f5d43..797c7c03 100644 --- a/src/solvers/cholmod.jl +++ b/src/solvers/cholmod.jl @@ -990,7 +990,7 @@ function Sparse(A::SparseMatrixCSC{<:Union{ComplexF16, ComplexF32}}, stype::Inte end # convert SparseVectors into CHOLMOD Sparse types through a mx1 CSC matrix -Sparse(A::SparseVector) = Sparse(SparseMatrixCSC(A)) +Sparse(A::SparseVector) = Sparse(SparseMatrixCSC(A), 0) function Sparse{Tv, Ti}(A::SparseMatrixCSC) where {Tv<:VTypes, Ti<:ITypes} o = Sparse{Tv, Ti}(A, 0) # check if array is symmetric and change stype if it is @@ -1135,6 +1135,7 @@ function SparseVector{Tv, Ti}(A::Sparse{Tv, Ti}) where {Tv, Ti<:ITypes} end args = _extract_args(s, Tv) s.sorted == 0 && _sort_buffers!(args...); + _trim_nz_builder!(args...) return SparseVector(args[1], args[4], args[5]) end diff --git a/src/solvers/spqr.jl b/src/solvers/spqr.jl index 5e9fa07e..72ebe550 100644 --- a/src/solvers/spqr.jl +++ b/src/solvers/spqr.jl @@ -85,7 +85,7 @@ function _qr!(ordering::Integer, tol::Real, econ::Integer, getCTX::Integer, # Free memory allocated by SPQR. This call will make sure that the # correct deallocator function is called and that the memory count in # the common struct is updated - Ti === Int64 ? + Ti === Int64 ? cholmod_l_free(n, sizeof(Ti), e, CHOLMOD.getcommon(Ti)) : cholmod_free(n, sizeof(Ti), e, CHOLMOD.getcommon(Ti)) end @@ -100,7 +100,7 @@ function _qr!(ordering::Integer, tol::Real, econ::Integer, getCTX::Integer, # Free memory allocated by SPQR. This call will make sure that the # correct deallocator function is called and that the memory count in # the common struct is updated - Ti === Int64 ? + Ti === Int64 ? cholmod_l_free(m, sizeof(Ti), hpinv, CHOLMOD.getcommon(Ti)) : cholmod_free(m, sizeof(Ti), hpinv, CHOLMOD.getcommon(Ti)) end diff --git a/src/solvers/umfpack.jl b/src/solvers/umfpack.jl index 7a8458d9..fc4b9c17 100644 --- a/src/solvers/umfpack.jl +++ b/src/solvers/umfpack.jl @@ -471,7 +471,8 @@ function lu!(F::UmfpackLU{Tv, Ti}, S::AbstractSparseMatrixCSC; return lu!(F; reuse_symbolic, check, q) end -function lu!(F::UmfpackLU; check::Bool=true, reuse_symbolic::Bool=true, q=nothing) +function lu!(F::UmfpackLU{Tv, Ti}; check::Bool=true, reuse_symbolic::Bool=true, + q=nothing) where {Tv, Ti} if !reuse_symbolic && _isnotnull(F.symbolic) F.symbolic = Symbolic{Tv, Ti}(C_NULL) end diff --git a/src/sparseconvert.jl b/src/sparseconvert.jl index 479654b7..80854331 100644 --- a/src/sparseconvert.jl +++ b/src/sparseconvert.jl @@ -280,4 +280,3 @@ function _sparse_gen(m, n, newcolptr, newrowval, newnzval) newcolptr[1] = 1 SparseMatrixCSC(m, n, newcolptr, newrowval, newnzval) end - diff --git a/src/sparsematrix.jl b/src/sparsematrix.jl index 6aadb355..73dd2ba8 100644 --- a/src/sparsematrix.jl +++ b/src/sparsematrix.jl @@ -4513,7 +4513,7 @@ function Base.swaprows!(A::AbstractSparseMatrixCSC, i, j) iidx = searchsortedfirst(@view(rows[rr]), i) has_i = iidx <= length(rr) && rows[rr[iidx]] == i - jrange = has_i ? (iidx:last(rr)) : rr + jrange = has_i ? (rr[iidx]:last(rr)) : rr jidx = searchsortedlast(@view(rows[jrange]), j) has_j = jidx != 0 && rows[jrange[jidx]] == j @@ -4527,18 +4527,16 @@ function Base.swaprows!(A::AbstractSparseMatrixCSC, i, j) # Update the rowval and then rotate both nonzeros # and the remaining rowvals into the correct place rows[rr[iidx]] = j - jidx == 0 && continue rotate_range = rr[iidx]:jrange[jidx] - circshift!(@view(vals[rotate_range]), 1) - circshift!(@view(rows[rotate_range]), 1) + circshift!(@view(vals[rotate_range]), -1) + circshift!(@view(rows[rotate_range]), -1) else # Same as i, but in the opposite direction @assert has_j rows[jrange[jidx]] = i - iidx > length(rr) && continue rotate_range = rr[iidx]:jrange[jidx] - circshift!(@view(vals[rotate_range]), -1) - circshift!(@view(rows[rotate_range]), -1) + circshift!(@view(vals[rotate_range]), 1) + circshift!(@view(rows[rotate_range]), 1) end end return nothing diff --git a/src/sparsevector.jl b/src/sparsevector.jl index 1d7daf0b..65077517 100644 --- a/src/sparsevector.jl +++ b/src/sparsevector.jl @@ -1704,7 +1704,7 @@ for (fun, comp, word) in ((:findmin, :(<), "minimum"), (:findmax, :(>), "maximum m == n && return val, index nzinds = nonzeroinds(x) zeroval = f(zero(T)) - $comp(val, zeroval) && return val, nzinds[index] + ($comp(val, zeroval) || isnan(val)) && return val, nzinds[index] # we need to find the first zero, which could be stored or implicit # we try to avoid findfirst(iszero, x) sindex = findfirst(_iszero, nzvals) # first stored zero, if any diff --git a/test/cholmod.jl b/test/cholmod.jl index 4224c002..f5e9ef93 100644 --- a/test/cholmod.jl +++ b/test/cholmod.jl @@ -806,6 +806,14 @@ end @test chI \ sparseI ≈ sparseI end +@testset "Issue 630" begin + sparseI = sparse(1.0I, 1, 1) + @test cholesky(sparseI) \ sparse([1.0]) == [1] + sparseI = sparse(1.0I, 2, 2) + res = cholesky(sparseI) \ spzeros(2) + @test isempty(nonzeros(res)) +end + @testset "Real factorization and complex rhs" begin A = sprandn(5, 5, 0.4) |> t -> t't + I B = complex.(randn(5, 5), randn(5, 5)) @@ -1017,7 +1025,7 @@ end end f = ones(size(K, 1)) - u = K \ f + u = K \ f residual = norm(f - K * u) / norm(f) @test residual < 1e-6 end diff --git a/test/fixed.jl b/test/fixed.jl index 59c10346..e477363d 100644 --- a/test/fixed.jl +++ b/test/fixed.jl @@ -118,7 +118,7 @@ end @test f(x, y, z) == 0 t = similar(x) @test typeof(t) == typeof(x) - @test struct_eq(t, x) + @test struct_eq(t, x) end @testset "Issue #190" begin diff --git a/test/forbidproperties.jl b/test/forbidproperties.jl index 6550462c..d07d9cab 100644 --- a/test/forbidproperties.jl +++ b/test/forbidproperties.jl @@ -1,5 +1,18 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license using SparseArrays -Base.getproperty(::SparseMatrixCSC, ::Symbol) = error("use accessor function") -Base.getproperty(::SparseVector, ::Symbol) = error("use accessor function") +# Only define each `getproperty` override if it isn't already defined. This +# file is `include`d from multiple sibling test modules, and without this +# guard each subsequent include would re-set the same `Base` methods, +# producing spurious method-overwrite warnings (depending on `--warn-overwrite`). +# `hasmethod` is not sufficient here because the generic `Base.getproperty` +# fallback for `Any` always exists; we instead check whether `which` returns +# that fallback method. +let fallback = which(Base.getproperty, Tuple{Any, Symbol}) + if which(Base.getproperty, Tuple{SparseMatrixCSC, Symbol}) === fallback + Base.getproperty(::SparseMatrixCSC, ::Symbol) = error("use accessor function") + end + if which(Base.getproperty, Tuple{SparseVector, Symbol}) === fallback + Base.getproperty(::SparseVector, ::Symbol) = error("use accessor function") + end +end diff --git a/test/issues.jl b/test/issues.jl index f198812a..9f46a639 100644 --- a/test/issues.jl +++ b/test/issues.jl @@ -805,6 +805,13 @@ end 7 16 4]) end +@testset "Issue #574" begin + a = spzeros(Float32, Int16, 2, 3) + v = spzeros(Float32, Int16, 2) + @test eltype(rowvals(zero(a))) <: Int16 + @test eltype(rowvals(zero(v))) <: Int16 +end + end # SparseTestsBase end # module diff --git a/test/sparsematrix_constructors_indexing.jl b/test/sparsematrix_constructors_indexing.jl index a0396f40..52b09677 100644 --- a/test/sparsematrix_constructors_indexing.jl +++ b/test/sparsematrix_constructors_indexing.jl @@ -1426,6 +1426,21 @@ using Base: swaprows!, swapcols! f!(Scopy, i, j); f!(Sdense, i, j) @test Scopy == Sdense end + + for (A, i, j) in ( + (sparse([1.0 2.0 3.0; + 0.0 0.0 0.0; + 4.0 5.0 6.0]), 1, 2), + (sparse([1.0 0.0 5.0; + 0.0 2.0 0.0; + 0.0 3.0 6.0; + 7.0 4.0 0.0]), 1, 2), + (sparse(reshape([1.0, 2.0, 3.0, 4.0, 0.0, 0.0], 6, 1)), 2, 6)) + Scopy = copy(A) + Sdense = Array(A) + swaprows!(Scopy, i, j); swaprows!(Sdense, i, j) + @test Scopy == Sdense + end end @testset "sprandn with type $T" for T in (Float64, Float32, Float16, ComplexF64, ComplexF32, ComplexF16) diff --git a/test/sparsevector.jl b/test/sparsevector.jl index c91f0d69..a3ad5fe0 100644 --- a/test/sparsevector.jl +++ b/test/sparsevector.jl @@ -1052,6 +1052,11 @@ end @test all(!iszero, v) @test !any(iszero, v) end + + let v = sparse([0, NaN]) #issue #714 + @test findmin(v) === (NaN, 2) + @test findmax(v) === (NaN, 2) + end end ### linalg diff --git a/test/umfpack.jl b/test/umfpack.jl index 59dc9041..0a4525e2 100644 --- a/test/umfpack.jl +++ b/test/umfpack.jl @@ -408,7 +408,7 @@ end umfpack_report(x.b) end - @testset "Reuse symbolic LU factorization" begin + @testset "Do/do not reuse symbolic LU factorization" for reuse ∈ (true, false) A1 = sparse(increment!([0,4,1,1,2,2,0,1,2,3,4,4]), increment!([0,4,0,2,1,2,1,4,3,2,1,2]), [2.,1.,3.,4.,-1.,-3.,3.,9.,2.,1.,4.,2.], 5, 5) @@ -420,7 +420,7 @@ end b = Tv[8., 45., -3., 3., 19.] F = lu(A) umfpack_report(F) - lu!(F, B) + lu!(F, B; reuse_symbolic=reuse) umfpack_report(F) @test F\b ≈ B\b ≈ Matrix(B)\b @@ -429,14 +429,20 @@ end C[4, 3] = Tv(0) F = lu(A) umfpack_report(F) - @test_throws SingularException lu!(F, C) + @test_throws SingularException lu!(F, C; reuse_symbolic=reuse) # change of nonzero pattern D = copy(B) D[5, 1] = Tv(1.0) F = lu(A) umfpack_report(F) - @test_throws ArgumentError lu!(F, D) - umfpack_report(F) + if reuse + @test_throws ArgumentError lu!(F, D; reuse_symbolic=reuse) + umfpack_report(F) + else + lu!(F, D; reuse_symbolic=reuse) + umfpack_report(F) + @test F\b ≈ D\b ≈ Matrix(D)\b + end end end end