nvim-neotest / neotest

An extensible framework for interacting with tests within NeoVim.
MIT License
2.44k stars 123 forks source link

Feature request: PHPUnit Runner #25

Closed boonkerz closed 2 years ago

boonkerz commented 2 years ago

Anyone work on this Adapter?

olimorris commented 2 years ago

I considered doing this after I completed the work on the RSpec adapter. Big challenge will be that PHPUnit outputs in xml only (never understood why they removed the JSON option in previous versions). Guessing you'd need to add a luarocks dependency to be able to process it.

rcarriga commented 2 years ago

Funny enough I actually embedded https://github.com/manoelcampos/xml2lua inside the neotest libs with the idea of using it for code coverage but never got around to it. Can always use that :sweat_smile: Just call require("neotest.lib").xml.parse(<xml string>)

boonkerz commented 2 years ago

currently i working on this but is my first try in lua :)

the message says no tests found but the logfile is showing somthing:

DEBUG | 2022-06-14T15:08:44Z+0200 | .../packer/start/neotest/lua/neotest/client/events/init.lua:48 | Calling listener diagnostic for event discover_positions
DEBUG | 2022-06-14T15:08:44Z+0200 | ...k/packer/start/neotest/lua/neotest/client/state/init.lua:54 | New positions at ID /home/thomas/projekte/calc/tests/PreCalc/ParseTest.php
INFO | 2022-06-14T15:08:44Z+0200 | .../packer/start/neotest/lua/neotest/client/events/init.lua:46 | Emitting discover_positions event
DEBUG | 2022-06-14T15:08:44Z+0200 | .../packer/start/neotest/lua/neotest/client/events/init.lua:48 | Calling listener status for event discover_positions
DEBUG | 2022-06-14T15:08:44Z+0200 | .../packer/start/neotest/lua/neotest/client/events/init.lua:48 | Calling listener summary for event discover_positions
DEBUG | 2022-06-14T15:08:44Z+0200 | .../packer/start/neotest/lua/neotest/client/events/init.lua:48 | Calling listener diagnostic for event discover_positions
DEBUG | 2022-06-14T15:08:44Z+0200 | ...k/packer/start/neotest/lua/neotest/client/state/init.lua:54 | New positions at ID /home/thomas/projekte/calc/tests/PreCalc/ParseVariantTest.php

my query:

(method_declaration
        name: (name) @test.name)
        @test.definition 

 (namespace_definition
        name: (namespace_name) @namespace.name)
        @namespace.definition

query is tested with https://tree-sitter.github.io/tree-sitter/playground

rcarriga commented 2 years ago

Can you provide the adapter code as well? I can't really tell much from the logs

boonkerz commented 2 years ago

very minimal

---@diagnostic disable: undefined-field
local lib = require('neotest.lib')
local logger = require('neotest.logging')

---@type neotest.Adapter
local adapter = { name = 'neotest-phpunit' }

adapter.root = lib.files.match_root_pattern('composer.json')

function adapter.is_test_file(file_path)
  return vim.endswith(file_path, "Test.php")
end

function adapter.build_spec(args)
  local results_path = vim.fn.tempname() .. '.json'
  local tree = args.tree
  if not tree then
    return
  end
  local pos = args.tree:data()
  local testNamePattern = '.*'
  if pos.type == 'test' then
    testNamePattern = pos.name
  end

  local binary = 'phpunit'
  if vim.fn.filereadable('vendor/bin/phpunit') then
    binary = 'vendor/bin/phpunit'
  end

  local command = vim.tbl_flatten({
    binary,
    pos.path,
  })
  return {
    command = command,
    context = {
      results_path = results_path,
      file = pos.path,
    },
  }
end

function adapter.discover_positions(path)
    local query = [[
    (method_declaration
        name: (name) @test.name)
        @test.definition 

    (namespace_definition
        name: (namespace_name) @namespace.name)
        @namespace.definition
    ]]
  return lib.treesitter.parse_positions(path, query, { nested_namespace = true })
end

setmetatable(adapter, {
  __call = function()
    return adapter
  end,
})
boonkerz commented 2 years ago

Current Status

image

rcarriga commented 2 years ago

Ah so it's working :+1: I see you haven't parsed the class as Neotest's "namespace" type. The namespace in PHP is not really what Neotest means by namespace so the class would be more suitable

Geraint commented 2 years ago

@boonkerz Does your code detect tests which use the @test annotation, as well as methods named test*?

From the PHPUnit documentation:

Alternatively, you can use the @test annotation in a method’s docblock to mark it as a test method.

source.

boonkerz commented 2 years ago

@boonkerz Does your code detect tests which use the @test annotation, as well as methods named test*?

From the PHPUnit documentation:

Alternatively, you can use the @test annotation in a method’s docblock to mark it as a test method.

source.

currently not but i work on this :)

boonkerz commented 2 years ago

Ah so it's working +1 I see you haven't parsed the class as Neotest's "namespace" type. The namespace in PHP is not really what Neotest means by namespace so the class would be more suitable

yea currently happy does it do anything :)

my goal for the gui structure is the phpstorm version. image

boonkerz commented 2 years ago

but the xml is stupid because the failure is only 1 blob of text with \n

 <testcase name="testNotO" class="PSC\Library\Calc\Tests\Complex\firstTest" classname="PSC.Library.Calc.Tests.Complex.firstTest" file="/home/thomas/projekte/phpunit_test/tests/Complex/firstTest.php" line="13" assertions="1" time="0.003164">
        <failure type="PHPUnit\Framework\ExpectationFailedException">PSC\Library\Calc\Tests\Complex\firstTest::testNotO
Failed asserting that true is false.

/home/thomas/projekte/phpunit_test/tests/Complex/firstTest.php:15</failure>
      </testcase>
boonkerz commented 2 years ago

so usable for me :D a little bit slow.

image

olimorris commented 2 years ago

@boonkerz care to share your work so far?

boonkerz commented 2 years ago
---@diagnostic disable: undefined-field
local lib = require('neotest.lib')
local logger = require('neotest.logging')
local async = require('neotest.async')
local adapter = { name = 'neotest-phpunit' }

adapter.root = lib.files.match_root_pattern('composer.json')

function adapter.is_test_file(file_path)
  if string.match(file_path, "vendor") then
      return false
  end
  return vim.endswith(file_path, "Test.php")
end

function adapter.build_spec(args)
  local results_path = vim.fn.tempname() .. '.xml'
  local tree = args.tree
  if not tree then
    return
  end
  local pos = args.tree:data()
  local testNamePattern = '.*'
  if pos.type == 'test' then
    testNamePattern = pos.name
  end

  local binary = 'phpunit'
  if vim.fn.filereadable('vendor/bin/phpunit') then
    binary = 'vendor/bin/phpunit'
  end

  local command = vim.tbl_flatten({
    binary,
    pos.path,
    '--log-junit='..results_path 
  })
  logger.error('phpunit_command', command)
  return {
    command = command,
    context = {
      results_path = results_path,
      file = pos.path,
    },
  }
end

function adapter.results(spec, result)
  local results = {}

  local success, data = pcall(lib.files.read, spec.context.results_path)

  if not success then
    results = "{}"
  end
  local parsedXml = lib.xml.parse(data)
  for _, containerSuite in pairs(parsedXml.testsuites) do
    for __, testsuite in pairs(containerSuite.testsuite) do
        for ___,testcase in pairs(testsuite.testcase) do
            local error = { message = "Error", line = 15 }
            local alias_id = ""
            if testcase['_attr'] then
                alias_id = testcase._attr.file .. '::' .. testcase._attr.name
            elseif testcase["name"] then
                alias_id = testcase.file .. '::' .. testcase.name
            end

            if not testcase["failure"] then
                results[alias_id] = { status = "passed", short = "", output = ""}
            else
                local fname = async.fn.tempname()
                vim.fn.writefile({testcase.failure[1]}, fname)
                results[alias_id] = { status = "failed", short = testcase.failure[1], output = fname, errors = {error} }
            end
        end
    end
  end
  return results
end

local function generate_position_id(position, namespaces)
    local id = table.concat(
        vim.tbl_flatten({
          position.path,
          position.name,
        }),
        "::"
    )   
    logger.error("generate_position_id", id)
    return id
end

function adapter.discover_positions(path)
    local query = [[
    (method_declaration
        name: (name) @test.name
        (#match? @test.name "test"))
        @test.definition

    (namespace_definition
        name: (namespace_name) @namespace.name)
        @namespace.definition
    ]]
  return lib.treesitter.parse_positions(path, query, { nested_namespace = true, position_id = generate_position_id })
end

setmetatable(adapter, {
  __call = function()
    return adapter
  end,
})

return adapter
boonkerz commented 2 years ago

currently i not know what errors in results array does. dap integration and i love to see not the filenames in gui run this file does also not work :)

@test works currently not because i not know how to make the query.

olimorris commented 2 years ago

Any plans to put it in a repo? I'd like to work on the @test treesitter query.

Geraint commented 2 years ago

I'm not sure if this will be helpful, but I post in the hope that it will be.

I notice that it's possible to pass the PHPUnit command a --list-tests-xml argument.

This identifies test methods with:

It also identifies different test runs with @dataProvider data-sets (see documentation).

It does not include line numbers, though.

--list-tests-xml seems to need an output file, and you don't seem to be able to filter it by a particular file.

In contrast --list-tests outputs plain text to STDOUT, and you can limit it to a particular file.

boonkerz commented 2 years ago

link to repo https://github.com/boonkerz/neotest-phpunit

@Geraint thanks

olimorris commented 2 years ago

I haven't heard anything more from @boonkerz on the development of this adapter so I've advanced it quite a bit, here. Would really appreciate people using it in anger and for any bugs they find, raising an issue with an example test I can work against

rcarriga commented 2 years ago

Since we now have a documented adapter, going to close this out.