Skip to content

add intellij integration test structure #76

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Apr 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Tests
on:
pull_request:

permissions:
contents: read
checks: write
pull-requests: write

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 17
cache: 'gradle'

- name: Install Clojure
uses: DeLaGuardo/setup-clojure@master
with:
bb: '1.12.196'
cli: 1.12.0.1530

- name: Run tests
run: ./gradlew test

- name: Publish Test Report
uses: mikepenz/action-junit-report@v5
if: success() || failure() # always run even if the previous step fails
with:
report_paths: '**/build/test-results/test/TEST-*.xml'
simplified_summary: true
comment: false
1 change: 1 addition & 0 deletions bb.edn
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{:paths ["src/scripts"]
:tasks {tag scripts/tag
build-plugin scripts/build-plugin
test scripts/tests
install-plugin scripts/install-plugin
publish-plugin scripts/publish-plugin}}
40 changes: 37 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,24 @@ repositories {

dependencies {
implementation ("org.clojure:clojure:1.12.0")
implementation ("com.github.ericdallo:clj4intellij:0.7.1")
implementation ("com.github.ericdallo:clj4intellij:0.8.0")
implementation ("seesaw:seesaw:1.5.0")
implementation ("camel-snake-kebab:camel-snake-kebab:0.4.3")
implementation ("com.rpl:proxy-plus:0.0.9")
implementation ("dev.weavejester:cljfmt:0.13.0")
implementation ("com.github.clojure-lsp:clojure-lsp:2025.01.22-23.28.23")
implementation ("nrepl:nrepl:1.3.1")

testImplementation("junit:junit:latest.release")
testImplementation("org.junit.platform:junit-platform-launcher:latest.release")
testRuntimeOnly ("dev.clojurephant:jovial:0.4.2")
}

sourceSets {
main {
java.srcDirs("src/main", "src/gen")
if (project.gradle.startParameter.taskNames.contains("buildPlugin") ||
project.gradle.startParameter.taskNames.contains("clojureRepl") ||
project.gradle.startParameter.taskNames.contains("runIde")) {
resources.srcDirs("src/main/dev-resources")
}
Expand Down Expand Up @@ -146,6 +152,10 @@ tasks {
systemProperty("jb.consents.confirmation.enabled", "false")
}

test {
systemProperty("idea.mimic.jar.url.connection", "true")
}

signPlugin {
certificateChain.set(System.getenv("CERTIFICATE_CHAIN"))
privateKey.set(System.getenv("PRIVATE_KEY"))
Expand All @@ -165,6 +175,26 @@ tasks {
enabled = false
}

clojureRepl {
dependsOn("compileClojure")
classpath.from(sourceSets.main.get().runtimeClasspath
+ file("build/classes/kotlin/main")
+ file("build/clojure/main")
)
// doFirst {
// println(classpath.asPath)
// }
forkOptions.jvmArgs = listOf("--add-opens=java.desktop/java.awt=ALL-UNNAMED",
"--add-opens=java.desktop/java.awt.event=ALL-UNNAMED",
"--add-opens=java.desktop/sun.awt=ALL-UNNAMED",
"--add-opens=java.desktop/sun.font=ALL-UNNAMED",
"--add-opens=java.base/java.lang=ALL-UNNAMED",
"-Djava.system.class.loader=com.intellij.util.lang.PathClassLoader",
"-Didea.mimic.jar.url.connection=true",
"-Didea.force.use.core.classloader=true"
)
}

generateParser {
source.set("src/main/gramar/clojure.bnf")
targetRoot.set("src/gen")
Expand All @@ -180,14 +210,18 @@ tasks {
}
}

tasks.withType<Test>().configureEach {
useJUnitPlatform()
}

grammarKit {
jflexRelease.set("1.7.0-1")
grammarKitRelease.set("2021.1.2")
intellijRelease.set("203.7717.81")
}

clojure.builds.named("main") {
classpath.from(sourceSets.main.get().runtimeClasspath.asPath)
classpath.from(sourceSets.main.get().runtimeClasspath.asPath + "build/classes/kotlin/main")
checkAll()
aotAll()
reflection.set("fail")
Expand All @@ -213,6 +247,6 @@ fun fetchLatestLsp4ijNightlyVersion(): String {
println("Failed to fetch LSP4IJ nightly build version: ${e.message}")
}

val minVersion = "0.0.1-20231213-012910"
val minVersion = "0.12.1-20250404-161025"
return if (minVersion < onlineVersion) onlineVersion else minVersion
}
13 changes: 11 additions & 2 deletions src/main/clojure/com/github/clojure_lsp/intellij/editor.clj
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
(ns com.github.clojure-lsp.intellij.editor
(:require
[com.github.clojure-lsp.intellij.db :as db]
[com.github.clojure-lsp.intellij.editor :as editor])
[com.github.clojure-lsp.intellij.editor :as editor]
[com.github.ericdallo.clj4intellij.app-manager :as app-manager])
(:import
[com.intellij.openapi.editor Editor]
[com.intellij.openapi.editor CaretModel Editor LogicalPosition]
[com.intellij.openapi.fileEditor FileDocumentManager]
[com.intellij.openapi.project ProjectLocator]
[com.intellij.openapi.util.text StringUtil]
Expand All @@ -23,3 +24,11 @@
(defn guess-project-for [^VirtualFile file]
(or (.guessProjectForFile (ProjectLocator/getInstance) file)
(first (db/all-projects))))

(defn move-caret-to-position
"Moves the caret to the specified logical position in the editor."
[^Editor editor line column]
(let [caret ^CaretModel (.getCaretModel editor)
new-position (LogicalPosition. line column)]
@(app-manager/invoke-later!
{:invoke-fn (fn [] (.moveToLogicalPosition caret new-position))})))
3 changes: 3 additions & 0 deletions src/scripts/scripts.clj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
(shell "git push origin HEAD")
(shell "git push origin --tags"))

(defn tests []
(shell "./gradlew test"))

#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn build-plugin []
(shell "./gradlew buildPlugin"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
(ns com.github.clojure-lsp.intellij.slurp-action-test
(:require
[clojure.test :refer [deftest is]]
[com.github.clojure-lsp.intellij.editor :as editor]
[com.github.clojure-lsp.intellij.test-utils :as test-utils]
[com.github.ericdallo.clj4intellij.test :as clj4intellij.test])
(:import
[com.intellij.openapi.project Project]
[com.intellij.testFramework.fixtures CodeInsightTestFixture]))

(set! *warn-on-reflection* true)

(deftest slurp-action-test
"Tests the Forward Slurp editor action functionality in Clojure LSP.
This test:
1. Sets up a test project with a Clojure file
2. Opens the file in the editor
3. Sets up the LSP server
4. Moves the caret to a specific position
5. Executes the Forward Slurp action
6. Verifies the resulting text matches the expected output

The test ensures that the Forward Slurp action correctly modifies the code structure
by moving the closing parenthesis forward."
(let [project-name "clojure.sample-project"
{:keys [fixtures project deps-file]} (test-utils/setup-test-project project-name)
clj-file (.copyFileToProject ^CodeInsightTestFixture fixtures "foo.clj")]
(is (= project-name (.getName ^Project project)))
(is deps-file)

(let [editor (test-utils/open-file-in-editor fixtures clj-file)]
(test-utils/setup-lsp-server project)
(editor/move-caret-to-position editor 2 8)
(test-utils/run-editor-action "ClojureLSP.ForwardSlurp" project)
(clj4intellij.test/dispatch-all)

(.checkResultByFile ^CodeInsightTestFixture fixtures "foo_expected.clj")

(test-utils/teardown-test-project project))))
92 changes: 92 additions & 0 deletions src/test/clojure/com/github/clojure_lsp/intellij/test_utils.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
(ns com.github.clojure-lsp.intellij.test-utils
(:require
[com.github.clojure-lsp.intellij.client :as lsp-client]
[com.github.clojure-lsp.intellij.server :as server]
[com.github.ericdallo.clj4intellij.app-manager :as app-manager]
[com.github.ericdallo.clj4intellij.test :as clj4intellij.test])
(:import
[com.github.clojure_lsp.intellij.extension SettingsState]
[com.intellij.ide DataManager]
[com.intellij.openapi.actionSystem ActionManager]
[com.intellij.openapi.components ServiceManager]
[com.intellij.testFramework.fixtures CodeInsightTestFixture]))

(set! *warn-on-reflection* true)

(defn get-editor-text
"Returns the text content of the editor's document."
[^CodeInsightTestFixture fixture]
(-> fixture .getEditor .getDocument .getText))

(defn open-file-in-editor
"Opens a file in the editor and returns the editor instance."
[^CodeInsightTestFixture fixture file]
(let [project (.getProject fixture)]
(app-manager/write-command-action
project
(fn [] (.openFileInEditor fixture file)))
(.getEditor fixture)))

(defn run-editor-action
"Runs an editor action with the given ID for the specified project."
[action-id project]
(let [action (.getAction (ActionManager/getInstance) action-id)
context (.getDataContext (DataManager/getInstance))]
(app-manager/write-command-action
project
(fn []
(.actionPerformed
action
(com.intellij.openapi.actionSystem.AnActionEvent/createFromDataContext action-id nil context))))))

(defn wait-lsp-start
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason using the same approach from clj4intellij with promises makes the test to hang forever 😢 So, we decided to create a specific function in this project to use a timeout instead of promise to wait.

"Dispatches all events until the LSP server is started or the timeout is reached."
[{:keys [project millis timeout]
:or {millis 1000
timeout 10000}}]
(let [start-time (System/currentTimeMillis)]
(loop []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clojure.core.async/alts! is perfect for those do or timeout task, but it's ok to keep it as it is

(let [current-time (System/currentTimeMillis)
elapsed-time (- current-time start-time)
status (lsp-client/server-status project)]
(cond
(>= elapsed-time timeout)
(throw (ex-info "LSP server failed to start within timeout"
{:elapsed-time elapsed-time
:final-status status}))

(= status :started)
true

:else
(do
(clj4intellij.test/dispatch-all)
(Thread/sleep millis)
(recur)))))))

(defn teardown-test-project
"Shuts down all resources for the given project."
[project]
(server/shutdown! project))

(defn setup-test-project
"Sets up a test project with the given name and optional deps.edn content.
Returns a map with :fixture, :project, and :deps-file."
([project-name]
(setup-test-project project-name "{}"))
([project-name deps-content]
(let [fixtures (clj4intellij.test/setup project-name)
deps-file (.createFile fixtures "deps.edn" deps-content)
_ (.setTestDataPath fixtures "testdata")
project (.getProject fixtures)]
{:fixtures fixtures
:project project
:deps-file deps-file})))

(defn setup-lsp-server
"Sets up and waits for the LSP server to be ready."
[project]
(let [my-settings ^SettingsState (ServiceManager/getService SettingsState)]
(.loadState my-settings my-settings)
(clj4intellij.test/dispatch-all)
(wait-lsp-start {:project project})))
3 changes: 3 additions & 0 deletions testdata/foo.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(ns foo)

(println) "Oiii"
3 changes: 3 additions & 0 deletions testdata/foo_expected.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(ns foo)

(println "Oiii")