add_test handles empty strings differently from before?

Hi,

Consider the following minimal example:

CMAKE_MINIMUM_REQUIRED(VERSION 3.20 FATAL_ERROR)

PROJECT(tt)

set(include_dirs)
set(include_dir_options "$<$<BOOL:${include_dirs}>:-I;$<JOIN:${include_dirs},;-I;>>")

enable_testing()
add_test(NAME test COMMAND ${CMAKE_COMMAND} -E echo "${include_dir_options}"
         COMMAND_EXPAND_LISTS)

Now run the test using ctest -V

CMake 3.20.5 will show the test being run as:

1: Test command: /opt/cmake_3_20_5/bin/cmake "-E" "echo"

while CMake 3.29.6 will show it as:

1: Test command: /home/kees-jan/src/cmake/cmake-build/bin/cmake "-E" "echo" ""

the difference being that CMake 3.29.6 passes along an empty string
argument, while 3.20.5 does not. In my actual usecase, a custom tool is
being called that complains about the extra argument.

Normally, I’d remove the quotes from "${include_dir_options}", but that
keeps the generator expression from being expanded, resulting in
incorrect behavior

Why was the behavior changed? Can we get the old behavior back? Any
other tips for solving this?

Thanks!

Kees-Jan

I don’t know when the behavior changed, so you’d need to do a git bisect or some other way to work out precisely what changed it. But the new behavior is the more correct behavior. If you put quotes around an argument, you’re saying that there must be an argument there, even if it evaluates to an empty string. If you want empty evaluation to result in no argument being added, using it unquoted is what you should do.

When you use the value unquoted, you’re probably hitting the problem that the semicolons are causing argument splitting. Since you’re only using this as a string to echo to the output, you could replace semicolons with spaces in that printed value. Something like the following might get you close:

set(include_dirs)
set(include_dir_options "$<$<BOOL:${include_dirs}>:-I;$<JOIN:${include_dirs},;-I;>>")

string(REPLACE ";" " " echo_include_dir_options "${include_dir_options}")

enable_testing()
add_test(NAME test COMMAND ${CMAKE_COMMAND} -E echo ${echo_include_dir_options})

But you also mention that you have a real tool accepting these arguments, so I’m assuming the “echo” is just a simplified example. If you need real arguments to pass to a real tool, another choice might be to replace the semicolons with $<SEMICOLON> instead of a space. I don’t know if that will work, but COMMAND_EXPAND_LISTS will probably be needed in this case.

I would agree with you if generator expression expansion was done in both the quoted and unquoted case. Unfortunately that is not the case.

Your tip (thanks) does not work well in the case where include_dirs is not empty. I now have:

CMAKE_MINIMUM_REQUIRED(VERSION 3.20 FATAL_ERROR)

PROJECT(tt)

set(include_dirs)
list(APPEND include_dirs "dir1")
list(APPEND include_dirs "dir2")
list(APPEND include_dirs "dir3")
set(include_dir_options "$<$<BOOL:${include_dirs}>:-I;$<JOIN:${include_dirs},;-I;>>")
string(REPLACE ";" " " echo_include_dir_options "${include_dir_options}")

enable_testing()
add_test(NAME test COMMAND ${CMAKE_COMMAND} -E echo ${echo_include_dir_options})

Which gives me

1: Test command: /home/kees-jan/src/cmake/cmake-build/bin/cmake “-E” “echo” “-I dir1 dir2 dir3”

As you can see, "-I dir1 dir2 dir3" is now passed as a single argument with no -I in between the directories, instead of the intended "-I" "dir1" "-I" "dir2" "-I" "dir3"

Yes, that was the output I expected for the echo case. See the last paragraph of my last comment for how to modify things for your real script.

My apologies. I completely missed the fact that $<SEMICOLON> is a generator expression and therefore didn’t understand the last paragraph. Also I was wrong (as you pointed out) that I need quotes to have my generator expression expanded.

I’ve now hopefully correctly applied all of your tips, and ended up with

CMAKE_MINIMUM_REQUIRED(VERSION 3.20 FATAL_ERROR)

PROJECT(tt)

set(include_dirs)
set(include_dir_options "$<$<BOOL:${include_dirs}>:-I;$<JOIN:${include_dirs},;-I;>>")
string(REPLACE ";" "$<SEMICOLON>" echo_include_dir_options "${include_dir_options}")
enable_testing()
add_test(NAME test COMMAND ${CMAKE_COMMAND} -E echo ${echo_include_dir_options} COMMAND_EXPAND_LISTS)

Now, in spite of not having quoted ${echo_include_dir_options} any more, I’m still getting

1: Test command: /home/kees-jan/src/cmake/cmake-build/bin/cmake "-E" "echo" ""

It would be useful to pin down the exact commit where the behavior changed. I’m focused on other activities at the moment. If you want build CMake locally yourself and do a git bisect to find where the behavior changed, that would be most welcome. Once we know where/why it changes, we’ll be in a better position to know what’s involved in supporting the original behavior.

Absolutely!

1df3287bf6a49d410b81be670218259afc3474d4 is the first bad commit
commit 1df3287bf6a49d410b81be670218259afc3474d4
Author: Marc Chevrier <marc.chevrier@gmail.com>
Date:   Fri Jun 9 11:44:27 2023 +0200

    add_test: Restore support for empty test arguments
    
    This was regressed by refactoring in commit e08ba229ee (CMake code rely
    on cmList class for CMake lists management (part. 1), 2023-04-14,
    v3.27.0-rc1~174^2).  Fix it and add a test case.
    
    Fixes: #24986

 Source/cmTestGenerator.cxx                       |  3 ++-
 Tests/RunCMake/add_test/CheckEmptyArgument.cmake | 11 +++++++++++
 Tests/RunCMake/add_test/EmptyArgument.cmake      |  5 +++++
 Tests/RunCMake/add_test/RunCMakeTest.cmake       |  8 ++++++++
 4 files changed, 26 insertions(+), 1 deletion(-)
 create mode 100644 Tests/RunCMake/add_test/CheckEmptyArgument.cmake
 create mode 100644 Tests/RunCMake/add_test/EmptyArgument.cmake

Thanks for that. It looks like there may have been another fix and regression between 3.20.5 and 3.27.0-rc1 though. Looking at the referenced issue, the commit you referenced refers to changes for 3.27.0-rc1 that caused empty arguments to be dropped. This means somewhere before 3.27.0-rc1, empty arguments were already being kept, which is contrary to your observation for 3.20.5. There must have been another change in between. If you’re able to find where that was, that would also be helpful. In the meantime, I will create an issue in our issue tracker related to this. I think there may still be a bug in the latest implementation, but I need to dig into he code a bit more to understand what’s intended versus what’s actually happening.

I created issue 26272 to track this apparent regression, but it may have been intentional. I’ve added a link back to this discussion thread here for context.

Hi Craig,

I see the issue you’ve created describes the original problem I’ve reported. However, with the lessons learned from our discussion, I would now phrase the problem differently.

You pointed out that expanding quoted empty variables to quoted empty strings is more correct. I concur, and it is no doubt the goal of the first failing commit we uncovered. My only objection was that without quotes, I couldn’t get the generator expression to expand. You’ve since been teaching me about argument splitting and $<SEMICOLON>.

With all that, my new problem description would be that I feel that empty variables and variables that contain a generator expression that evaluates to nothing should be treated the same (regardless of them being quoted). I.e. in

set(empty)
set(empty_generator_expression "$<0:content>")
enable_testing()
add_test(NAME test COMMAND ${CMAKE_COMMAND} -E echo ${empty})
add_test(NAME test2 COMMAND ${CMAKE_COMMAND} -E echo ${empty_generator_expression})

I’d expect the same test to be run twice (with no empty string argument, because there are no quotes). However, that’s currently not the case. I get

1: Test command: /usr/bin/cmake "-E" "echo"

and

2: Test command: /usr/bin/cmake "-E" "echo" ""

I’d argue that if I had used quotes, they should both have an empty string argument (and that part is indeed working correctly), but now that I don’t use quotes, they both should not. This is were the case with the generator expression goes wrong.

Does that help?

I’ve been doing some additional testing. This new problem I’m describing is already present in 3.20.5, so I can’t really call it a (recent) regression. This means that my original use-case would not have worked with that version either, and I have found a bug on our side :wink:

That does make me turn full circle to my original problem: In the scenario where there are no include directories, I would like there to be no (empty) arguments to my test. How’s that done? :wink:

I typed that, and immediately felt uncomfortable. I knew things worked well before. I just couldn’t explain why at the time. Thus, I did some additional experimentation.

Consider

add_test(NAME test COMMAND ${CMAKE_COMMAND} -E echo "")

That’s a quoted empty string, and it should be passed on to the test. Both 3.20 and 3.29 handle that case correctly. That also tells me that the regresson of empty strings not being supported happened somewhere after 3.20.

Next consider

add_test(NAME test COMMAND ${CMAKE_COMMAND} -E echo "" COMMAND_EXPAND_LISTS)

Now that, at least to me, is a quoted empty list. And empty lists (quoted or otherwise) should expand to absolutely nothing. 3.20 seems to agree with me, but that changed after commit 1df3287b cited above.

Now I’ll happily agree that there is some room for interpretation. After all, without the presence of a semicolon, it is hard to tell whether something is a list or not. But without any statistics to back me up, I’d still argue that empty lists are more common than empty strings, and therefore we should, in this case, err on the side of assuming that things are lists.